Java Socket 聊天室 - 从0开始开发教程
Java Socket 聊天室 - 从0开始开发教程
项目概述
这是一个基于 Java Socket 的网络聊天室应用,采用 C/S(客户端/服务端)架构,支持以下功能:
- 用户注册/登录/找回密码
- 私聊和群聊
- 文件传输(私聊和群聊)
- 窗口抖动功能
- 在线用户列表实时更新
- 群组管理(创建、搜索、加入)
- 自定义聊天字体
- 截图功能
技术栈
- Java SE - 核心编程语言
- Java Swing - GUI界面开发
- Java Socket - 网络通信
- Java IO - 数据传输
- 多线程 - 并发处理
项目结构
NewChatRoom/
├── src/
│ ├── common/ # 公共类(客户端和服务端共享)
│ │ ├── Message.java # 消息实体类
│ │ └── Group.java # 群组实体类
│ ├── server/ # 服务端代码
│ │ ├── ChatServer.java # 服务端主类
│ │ ├── handlers/
│ │ │ ├── ClientHandler.java # 客户端连接处理
│ │ │ └── MessageHandler.java # 消息处理
│ │ ├── managers/
│ │ │ ├── UserManager.java # 用户管理
│ │ │ ├── OnlineUserManager.java # 在线用户管理
│ │ │ └── GroupManager.java # 群组管理
│ │ └── broadcast/
│ │ └── BroadcastService.java # 广播服务
│ └── client/ # 客户端代码
│ ├── ChatClient.java # 客户端主类
│ ├── MessageListener.java # 消息监听线程
│ ├── ScreenshotManager.java # 截图管理
│ ├── UIComponentFactory.java # UI组件工厂
│ ├── network/
│ │ └── NetworkManager.java # 网络管理
│ ├── handler/
│ │ └── MessageHandler.java # 消息处理器
│ ├── managers/
│ │ ├── ChatManager.java # 聊天管理
│ │ ├── FileManager.java # 文件管理
│ │ ├── DataManager.java # 数据管理
│ │ ├── WindowManager.java # 窗口管理
│ │ └── AuthenticationManager.java # 认证管理
│ └── ui/
│ ├── ChatMainUI.java # 聊天主界面
│ ├── LoginRegisterUI.java # 登录注册界面
│ └── UIComponentFactory.java # UI组件工厂
项目架构图
1. 整体架构层次结构图
以下是项目的整体架构层次结构图,展示了客户端、服务端、公共模块之间的组织关系:
graph TB subgraph "客户端层" A1[ChatClient] A2[MessageListener] A3[ScreenshotManager] A4[UIComponentFactory] end subgraph "网络层" B1[NetworkManager] end subgraph "客户端处理器层" C1[MessageHandler] end subgraph "客户端管理器层" D1[ChatManager] D2[FileManager] D3[DataManager] D4[WindowManager] D5[AuthenticationManager] end subgraph "客户端UI层" E1[ChatMainUI] E2[LoginRegisterUI] end subgraph "公共层" F1[Message] F2[Group] end subgraph "服务端" G1[ChatServer] G2[ClientHandler] G3[MessageHandler] G4[BroadcastService] end subgraph "服务端管理器层" H1[UserManager] H2[OnlineUserManager] H3[GroupManager] end A1 --> B1 A1 --> C1 A1 --> D1 A1 --> D2 A1 --> D3 A1 --> D4 A1 --> D5 A1 --> E1 A1 --> E2 B1 --> F1 C1 --> F1 D1 --> F1 D2 --> F1 D5 --> F1 G1 --> G2 G1 --> G3 G1 --> G4 G2 --> F1 G3 --> F1 G4 --> F1 G2 --> H1 G2 --> H2 G2 --> H3 G3 --> H1 G3 --> H2 G3 --> H3
2. UML类图
以下是项目中主要类的关系图,展示各个类之间的继承、关联、依赖关系:
classDiagram class Message { <<enumeration>> +Type type +String sender +String receiver +String content +List~String~ onlineUsers +String fileName +long fileSize +byte[] fileData +String groupId +String groupName +List~Group~ groupList +String password +Message(Type, String, String, String) +Message(Type, String) +getType() +setType() +getSender() +setSender() +getReceiver() +setReceiver() +getContent() +setContent() +getOnlineUsers() +setOnlineUsers() +getFileName() +setFileName() +getFileSize() +setFileSize() +getFileData() +setFileData() +getGroupId() +setGroupId() +getGroupName() +setGroupName() +getGroupList() +setGroupList() +getPassword() +setPassword() } class Group { +String groupId +String groupName +List~String~ members +Group(String, String) +addMember(String) +removeMember(String) +getGroupId() +setGroupId() +getGroupName() +setGroupName() +getMembers() +setMembers() } class ChatClient { +String serverIp +String username +NetworkManager networkManager +MessageHandler messageHandler +UIComponentFactory uiComponentFactory +ChatManager chatManager +FileManager fileManager +DataManager dataManager +WindowManager windowManager +login(String, String, String) +initChatUI() +startMessageListener() +sendMessage() +selectFile() +sendFile() +saveBytesToFile() +updateUserList() +updateGroupList() +sendShakeMessage() +shakeWindow() +resetSocket() } class NetworkManager { +String serverIp +Socket socket +ObjectOutputStream oos +ObjectInputStream ois +connectToServer() +sendMessage(Message) +receiveMessage() +resetSocket() } class MessageHandler { +ChatClient chatClient +ChatMainUI chatMainUI +handleMessage(Message) } class ChatManager { +ChatClient chatClient +sendMessage() +sendShake() +sendCreateGroupRequest() +sendSearchGroupRequest() +sendJoinGroupRequest() } class FileManager { +ChatClient chatClient +File selectedFile +selectFile() +sendFile() +captureAndSendScreenshot() } class DataManager { +ChatClient chatClient +updateUserList() +updateGroupList() +saveBytesToFile() } class WindowManager { +ChatClient chatClient +shakeWindow() +isShaking() } class AuthenticationManager { +ChatClient chatClient +register(String, String) +login(String, String, String) +findPassword(String) +resetPassword(String) } class ChatMainUI { +ChatClient chatClient +JTextArea chatArea +JTextField inputField +JComboBox~String~ chatTypeBox +JComboBox~String~ targetBox +initChatUI(String) +getChatArea() +getInputField() +getChatTypeBox() +getTargetBox() } class LoginRegisterUI { +JFrame frame +JTextField loginAccountField +JPasswordField loginPwdField +JTextField registerAccountField +JPasswordField registerPwdField +performLogin() +performRegister() +performFindPassword() } class ChatServer { +UserManager userManager +OnlineUserManager onlineUserManager +GroupManager groupManager +BroadcastService broadcastService +start() } class ClientHandler { +Socket socket +MessageHandler messageHandler +run() +cleanup() +getOutputStream() } class ServerMessageHandler { +UserManager userManager +OnlineUserManager onlineUserManager +GroupManager groupManager +BroadcastService broadcastService +String currentUsername +handleMessage(Message) +setCurrentUsername(String) } class BroadcastService { +OnlineUserManager onlineUserManager +GroupManager groupManager +broadcastToAll(Message) +sendToUser(String, Message) +broadcastToGroup(String, Message) +broadcastOnlineUsers() +broadcastGroupList() } class UserManager { +Map~String, String~ userAuthMap +register(String, String) +authenticate(String, String) +userExists(String) +updatePassword(String, String) } class OnlineUserManager { +Map~String, ObjectOutputStream~ userMap +addUser(String, ObjectOutputStream) +removeUser(String) +getUserStream(String) +getOnlineUsers() +isUserOnline(String) } class GroupManager { +Map~String, Group~ groupMap +createGroup(String) +searchGroups(String) +getGroup(String) +joinGroup(String, String) +getAllGroups() } %% 关系定义 ChatClient ..> NetworkManager : uses ChatClient ..> MessageHandler : uses ChatClient ..> ChatManager : uses ChatClient ..> FileManager : uses ChatClient ..> DataManager : uses ChatClient ..> WindowManager : uses ChatClient ..> AuthenticationManager : uses ChatClient ..> ChatMainUI : uses ChatClient ..> LoginRegisterUI : uses MessageHandler --> ChatClient : handles MessageHandler --> ChatMainUI : updates ChatManager --> ChatClient : manages FileManager --> ChatClient : manages DataManager --> ChatClient : manages WindowManager --> ChatClient : manages AuthenticationManager --> ChatClient : manages NetworkManager ..> Message : sends/receives ChatManager ..> Message : creates FileManager ..> Message : creates AuthenticationManager ..> Message : creates ChatServer ..> ClientHandler : creates ChatServer ..> UserManager : uses ChatServer ..> OnlineUserManager : uses ChatServer ..> GroupManager : uses ChatServer ..> BroadcastService : uses ClientHandler --> ServerMessageHandler : contains ClientHandler ..> Message : receives ServerMessageHandler --> UserManager : uses ServerMessageHandler --> OnlineUserManager : uses ServerMessageHandler --> GroupManager : uses ServerMessageHandler --> BroadcastService : uses BroadcastService --> OnlineUserManager : uses BroadcastService --> GroupManager : uses Message ||--|| Group : contains ChatClient ||--o| ChatMainUI : contains ChatClient ||--o| LoginRegisterUI : contains
3. 网络通信架构图
以下是客户端与服务端的连接和通信机制图:
sequenceDiagram participant Client as 客户端 participant Network as 网络层 participant Server as 服务端 participant Handler as 消息处理器 participant Managers as 管理器层 Client->>Network: 连接到服务器 Network->>Server: TCP连接建立 Server->>Network: 连接确认 Network->>Client: 连接成功 loop 消息循环 Client->>Network: 发送Message对象 Network->>Server: 序列化传输 Server->>Handler: 解析消息 Handler->>Managers: 分发处理 alt 消息类型判断 case 私聊消息 Managers->>BroadcastService: 发送到指定用户 BroadcastService->>TargetClient: 消息转发 case 群聊消息 Managers->>BroadcastService: 发送到群组 BroadcastService->>GroupClients: 消息广播 case 文件传输 Managers->>BroadcastService: 发送文件数据 BroadcastService->>ReceivingClient: 文件转发 end end
4. 消息传递流程图
以下是消息从发送到接收的完整流程图:
flowchart TD A[用户操作 - 发送消息] --> B[ChatManager - 处理发送逻辑] B --> C{判断消息类型} C -->|私聊| D[创建PRIVATE_CHAT类型Message] C -->|群聊| E[创建GROUP_CHAT类型Message] C -->|文件| F[创建FILE_PRIVATE/GROUP类型Message] C -->|其他| G[创建对应类型Message] D --> H[NetworkManager - 发送Message到服务端] E --> H F --> H G --> H H --> I[网络传输到服务端] I --> J[ClientHandler - 接收Message] J --> K[ServerMessageHandler - 解析消息类型] K --> L{根据消息类型路由} L -->|登录| M[UserManager验证用户] L -->|私聊| N[BroadcastService发送给指定用户] L -->|群聊| O[BroadcastService广播给群成员] L -->|文件| P[转发文件数据] L -->|其他| Q[处理特定业务逻辑] M --> R[更新OnlineUserManager] N --> S[发送给目标用户] O --> T[广播给群内所有用户] P --> U[发送给接收方] Q --> V[执行相应操作] R --> W[客户端接收响应] S --> W T --> W U --> W V --> W W --> X[MessageHandler - 客户端处理消息] X --> Y[UI更新显示] Y --> Z[流程结束]
5. UI界面布局图
以下是聊天主界面的组件布局图:
graph LR A[ChatMainUI - 主窗口] --> B[顶部控制面板] A --> C[中间聊天显示区] A --> D[底部输入区] A --> E[右侧用户群组区] B --> B1[聊天类型选择框] B --> B2[目标选择框] B --> B3[字体选择框] B --> B4[字号选择框] C --> C1[聊天内容显示区 - JTextArea] C --> C2[滚动条] D --> D1[消息输入框 - JTextField] D --> D2[发送按钮] D --> D3[文件按钮] D --> D4[抖动按钮] D --> D5[截图按钮] D --> D6[创建群聊按钮] E --> E1[用户列表标签页] E --> E2[群组列表标签页] E1 --> E3[在线用户JList] E2 --> E4[群组JList] style A fill:#e1f5fe style B fill:#f3e5f5 style C fill:#e8f5e8 style D fill:#fff3e0 style E fill:#fce4ec
6. 数据流向图
以下是数据在各个模块间的流转过程图:
graph LR A[用户输入/操作] --> B[UI组件] B --> C[ChatManager/FileManager等管理器] C --> D[Message对象构建] D --> E[NetworkManager - 序列化发送] E --> F[网络传输] F --> G[服务端ClientHandler] G --> H[ServerMessageHandler] H --> I[UserManager/OnlineUserManager/GroupManager] I --> J[BroadcastService] J --> K[消息转发到目标客户端] K --> L[客户端NetworkManager接收] L --> M[MessageListener解包] M --> N[MessageHandler处理] N --> O[UI更新显示] O --> P[数据持久化/日志记录] style A fill:#ffebee style E fill:#e8f5e8 style F fill:#e3f2fd style G fill:#e8f5e8 style K fill:#e3f2fd style L fill:#e8f5e8 style O fill:#fff3e0
7. 系统运行时的组件交互图
以下是各管理器、处理器之间的协作关系图:
graph TB subgraph "客户端运行时" A[ChatClient] B[MessageListener] C[NetworkManager] D[MessageHandler] E[ChatManager] F[FileManager] G[DataManager] H[WindowManager] I[AuthenticationManager] J[UI组件] end subgraph "服务端运行时" K[ChatServer] L[ClientHandler] M[ServerMessageHandler] N[UserManager] O[OnlineUserManager] P[GroupManager] Q[BroadcastService] end A --> B A --> C A --> D A --> E A --> F A --> G A --> H A --> I A --> J B --> D C --> D D --> E D --> F D --> G K --> L K --> M K --> N K --> O K --> P K --> Q L --> M M --> N M --> O M --> P M --> Q Q --> O Q --> P
第一阶段:基础架构搭建
1.1 创建公共消息类
创建 common/Message.java,这是客户端和服务端通信的基础:
package common;
import java.io.Serializable;
import java.util.List;
/**
* 消息实体类,用于客户端和服务端之间的通信
*/
public class Message implements Serializable {
// 消息类型枚举
public enum Type {
LOGIN, // 登录
PRIVATE_CHAT, // 私聊
GROUP_CHAT, // 群聊
ONLINE_NOTIFY, // 上线通知
OFFLINE_NOTIFY, // 下线通知
GET_ONLINE_USERS, // 获取在线用户
ONLINE_USERS, // 在线用户列表
FILE_PRIVATE, // 私聊文件
FILE_GROUP, // 群聊文件
SHAKE, // 窗口抖动
CREATE_GROUP, // 创建群组
SEARCH_GROUP, // 搜索群组
JOIN_GROUP, // 加入群组
GROUP_LIST, // 群组列表
REGISTER, // 注册
REGISTER_RESPONSE, // 注册响应
FIND_PASSWORD, // 找回密码
FIND_PASSWORD_RESPONSE,
RESET_PASSWORD, // 重置密码
RESET_PASSWORD_RESPONSE
}
private Type type; // 消息类型
private String sender; // 发送者
private String receiver; // 接收者
private String content; // 消息内容
private List<String> onlineUsers; // 在线用户列表
// 文件传输相关字段
private String fileName;
private long fileSize;
private byte[] fileData;
// 群聊相关字段
private String groupId;
private String groupName;
private List<Group> groupList;
private String password; // 密码字段
// 构造函数
public Message(Type type, String sender, String receiver, String content) {
this.type = type;
this.sender = sender;
this.receiver = receiver;
this.content = content;
}
public Message(Type type, String sender) {
this.type = type;
this.sender = sender;
this.content = "";
}
// Getter 和 Setter 方法
public Type getType() { return type; }
public void setType(Type type) { this.type = type; }
public String getSender() { return sender; }
public void setSender(String sender) { this.sender = sender; }
public String getReceiver() { return receiver; }
public void setReceiver(String receiver) { this.receiver = receiver; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public List<String> getOnlineUsers() { return onlineUsers; }
public void setOnlineUsers(List<String> onlineUsers) { this.onlineUsers = onlineUsers; }
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public long getFileSize() { return fileSize; }
public void setFileSize(long fileSize) { this.fileSize = fileSize; }
public byte[] getFileData() { return fileData; }
public void setFileData(byte[] fileData) { this.fileData = fileData; }
public String getGroupId() { return groupId; }
public void setGroupId(String groupId) { this.groupId = groupId; }
public String getGroupName() { return groupName; }
public void setGroupName(String groupName) { this.groupName = groupName; }
public List<Group> getGroupList() { return groupList; }
public void setGroupList(List<Group> groupList) { this.groupList = groupList; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
关键点说明:
- 实现
Serializable接口,使对象可以通过网络传输 - 使用枚举定义消息类型,便于管理和扩展
- 包含多种构造函数,适应不同场景
- 提供完整的getter和setter方法
1.2 创建群组实体类
创建 common/Group.java:
package common;
import java.util.ArrayList;
import java.util.List;
public class Group implements java.io.Serializable {
private String groupId; // 唯一群ID
private String groupName; // 群名称
private List<String> members; // 群成员列表
public Group(String groupId, String groupName) {
this.groupId = groupId;
this.groupName = groupName;
this.members = new ArrayList<>();
}
// 添加群成员
public void addMember(String username) {
if (!members.contains(username)) {
members.add(username);
}
}
// 移除群成员
public void removeMember(String username) {
members.remove(username);
}
// Getter 和 Setter
public String getGroupId() { return groupId; }
public void setGroupId(String groupId) { this.groupId = groupId; }
public String getGroupName() { return groupName; }
public void setGroupName(String groupName) { this.groupName = groupName; }
public List<String> getMembers() { return members; }
public void setMembers(List<String> members) { this.members = members; }
}
第二阶段:服务端开发
2.1 用户管理器
创建 server/managers/UserManager.java:
在服务端开发中,用户管理器是整个系统的安全核心。下面是用户管理器与其他组件的交互关系图:
graph TD A[UserManager] --> B[用户注册] A --> C[用户登录验证] A --> D[密码找回验证] A --> E[密码更新] B --> F[检查用户名唯一性] C --> G[验证密码匹配] D --> H[验证账户存在] E --> I[更新密码存储] F --> J[ConcurrentHashMap存储] G --> J H --> J I --> J J --> K[持久化存储(可选)] style A fill:#e3f2fd style J fill:#f3e5f5
package server.managers;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 用户管理类
* 负责用户注册、登录验证、密码找回等功能
*/
public class UserManager {
private final Map<String, String> userAuthMap; // 用户名 -> 密码
public UserManager() {
this.userAuthMap = new ConcurrentHashMap<>();
}
/**
* 用户注册
*/
public boolean register(String username, String password) {
if (userAuthMap.containsKey(username)) {
return false; // 用户已存在
}
userAuthMap.put(username, password);
return true;
}
/**
* 验证用户登录
*/
public boolean authenticate(String username, String password) {
String storedPassword = userAuthMap.get(username);
return storedPassword != null && storedPassword.equals(password);
}
/**
* 验证账号是否存在
*/
public boolean userExists(String username) {
return userAuthMap.containsKey(username);
}
/**
* 更新密码
*/
public boolean updatePassword(String username, String newPassword) {
if (!userAuthMap.containsKey(username)) {
return false;
}
userAuthMap.put(username, newPassword);
return true;
}
}
关键点说明:
- 使用
ConcurrentHashMap确保线程安全 - 提供完整的用户生命周期管理功能
- 实现密码验证和找回机制
2.2 在线用户管理器
创建 server/managers/OnlineUserManager.java:
在线用户管理器负责实时跟踪连接的用户,其内部结构如下:
graph LR A[OnlineUserManager] --> B[用户连接映射表] A --> C[用户状态管理] A --> D[在线用户列表] B --> E[用户名 -> ObjectOutputStream] C --> F[添加用户] C --> G[移除用户] C --> H[检查在线状态] D --> I[获取在线用户列表] E --> J[ConcurrentHashMap存储] F --> J G --> J H --> J I --> J style A fill:#e8f5e8 style J fill:#f3e5f5
package server.managers;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 在线用户管理类
* 负责管理在线用户的连接和状态
*/
public class OnlineUserManager {
private final Map<String, ObjectOutputStream> userMap; // 用户名 -> 输出流
public OnlineUserManager() {
this.userMap = new ConcurrentHashMap<>();
}
/**
* 添加在线用户
*/
public void addUser(String username, ObjectOutputStream oos) {
userMap.put(username, oos);
}
/**
* 移除在线用户
*/
public void removeUser(String username) {
userMap.remove(username);
}
/**
* 获取用户输出流
*/
public ObjectOutputStream getUserStream(String username) {
return userMap.get(username);
}
/**
* 获取在线用户列表
*/
public List<String> getOnlineUsers() {
return new ArrayList<>(userMap.keySet());
}
/**
* 检查用户是否在线
*/
public boolean isUserOnline(String username) {
return userMap.containsKey(username);
}
}
2.3 群组管理器
创建 server/managers/GroupManager.java:
群组管理器的结构和关系如下:
graph TD A[GroupManager] --> B[群组创建] A --> C[群组搜索] A --> D[群组获取] A --> E[群组成员管理] B --> F[生成UUID] B --> G[创建Group对象] C --> H[关键词匹配] D --> I[按ID查找] E --> J[添加成员] E --> K[移除成员] F --> L[Group对象存储] G --> L H --> L I --> L J --> L K --> L L --> M[ConcurrentHashMap] style A fill:#fff3e0 style L fill:#f3e5f5 style M fill:#e1f5fe
package server.managers;
import common.Group;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 群组管理类
* 负责群组的创建、查找、成员管理等功能
*/
public class GroupManager {
private final Map<String, Group> groupMap;
public GroupManager() {
this.groupMap = new ConcurrentHashMap<>();
}
/**
* 创建群组
*/
public Group createGroup(String groupName) {
String groupId = UUID.randomUUID().toString();
Group group = new Group(groupId, groupName);
groupMap.put(groupId, group);
return group;
}
/**
* 搜索群组
*/
public List<Group> searchGroups(String keyword) {
List<Group> result = new ArrayList<>();
for (Group group : groupMap.values()) {
if (group.getGroupName().contains(keyword)) {
result.add(group);
}
}
return result;
}
/**
* 获取群组
*/
public Group getGroup(String groupId) {
return groupMap.get(groupId);
}
/**
* 加入群组
*/
public boolean joinGroup(String groupId, String username) {
Group group = groupMap.get(groupId);
if (group != null) {
group.addMember(username);
return true;
}
return false;
}
/**
* 获取所有群组
*/
public List<Group> getAllGroups() {
return new ArrayList<>(groupMap.values());
}
}
2.4 广播服务
创建 server/broadcast/BroadcastService.java:
广播服务的工作流程图:
graph LR A[BroadcastService] --> B[广播到所有用户] A --> C[发送给指定用户] A --> D[广播到群组] A --> E[广播在线用户列表] A --> F[广播群组列表] B --> G[遍历所有在线用户] B --> H[发送消息到每个用户] C --> I[获取目标用户输出流] C --> J[发送消息] D --> K[获取群组成员列表] D --> L[向每个成员发送消息] E --> M[获取在线用户列表] E --> N[创建ONLINE_USERS消息] E --> O[广播给所有用户] F --> P[获取群组列表] F --> Q[创建GROUP_LIST消息] F --> R[广播给所有用户] style A fill:#fce4ec style G fill:#e8f5e8 style I fill:#e8f5e8 style K fill:#e8f5e8
package server.broadcast;
import common.Group;
import common.Message;
import server.managers.GroupManager;
import server.managers.OnlineUserManager;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.List;
/**
* 广播服务类
* 负责向客户端广播消息
*/
public class BroadcastService {
private final OnlineUserManager onlineUserManager;
private final GroupManager groupManager;
public BroadcastService(OnlineUserManager onlineUserManager, GroupManager groupManager) {
this.onlineUserManager = onlineUserManager;
this.groupManager = groupManager;
}
/**
* 广播消息给所有在线用户
*/
public void broadcastToAll(Message message) throws IOException {
for (String username : onlineUserManager.getOnlineUsers()) {
ObjectOutputStream oos = onlineUserManager.getUserStream(username);
if (oos != null) {
oos.writeObject(message);
oos.flush();
}
}
}
/**
* 发送消息给指定用户
*/
public void sendToUser(String username, Message message) throws IOException {
ObjectOutputStream oos = onlineUserManager.getUserStream(username);
if (oos != null) {
oos.writeObject(message);
oos.flush();
}
}
/**
* 广播消息给群组成员
*/
public void broadcastToGroup(String groupId, Message message) throws IOException {
Group group = groupManager.getGroup(groupId);
if (group != null) {
for (String member : group.getMembers()) {
sendToUser(member, message);
}
}
}
/**
* 广播在线用户列表
*/
public void broadcastOnlineUsers() throws IOException {
List<String> onlineUsers = onlineUserManager.getOnlineUsers();
Message usersMsg = new Message(Message.Type.ONLINE_USERS, "服务器");
usersMsg.setOnlineUsers(onlineUsers);
broadcastToAll(usersMsg);
}
/**
* 广播群组列表
*/
public void broadcastGroupList() throws IOException {
List<Group> groupList = groupManager.getAllGroups();
Message groupsMsg = new Message(Message.Type.GROUP_LIST, "服务器");
groupsMsg.setGroupList(groupList);
broadcastToAll(groupsMsg);
}
}
2.5 消息处理器
创建 server/handlers/MessageHandler.java:
服务端消息处理器的处理流程图:
flowchart TD A[MessageHandler.handleMessage()] --> B{判断消息类型} B -->|LOGIN| C[处理登录请求] B -->|PRIVATE_CHAT| D[处理私聊消息] B -->|GROUP_CHAT| E[处理群聊消息] B -->|FILE_PRIVATE| F[处理私聊文件] B -->|FILE_GROUP| G[处理群聊文件] B -->|SHAKE| H[处理窗口抖动] B -->|CREATE_GROUP| I[处理创建群组] B -->|SEARCH_GROUP| J[处理搜索群组] B -->|JOIN_GROUP| K[处理加入群组] B -->|GET_ONLINE_USERS| L[处理获取在线用户] B -->|REGISTER| M[处理注册请求] B -->|FIND_PASSWORD| N[处理找回密码] B -->|RESET_PASSWORD| O[处理重置密码] C --> C1[验证用户名密码] C --> C2[添加到在线用户列表] C --> C3[广播登录成功消息] C --> C4[广播在线用户列表] D --> D1[转发给指定用户] E --> E1[转发给群组所有成员] F --> F1[转发给指定用户] G --> G1[转发给群组所有成员] H --> H1[转发给指定用户] I --> I1[创建新群组] I --> I2[广播群组列表] I --> I3[返回创建成功消息] J --> J1[搜索匹配群组] J --> J2[返回搜索结果] K --> K1[添加用户到群组] K --> K2[广播群组列表] K --> K3[返回加入结果] L --> L1[获取在线用户列表] L --> L2[广播给所有用户] M --> M1[验证用户名唯一性] M --> M2[注册新用户] M --> M3[返回注册结果] N --> N1[验证账号存在] N --> N2[返回验证结果] O --> O1[更新用户密码] O --> O2[返回更新结果]
package server.handlers;
import common.Group;
import common.Message;
import server.broadcast.BroadcastService;
import server.managers.GroupManager;
import server.managers.OnlineUserManager;
import server.managers.UserManager;
import java.io.IOException;
import java.util.List;
/**
* 消息处理器类
* 负责处理从客户端接收到的各种消息类型
*/
public class MessageHandler {
private final UserManager userManager;
private final OnlineUserManager onlineUserManager;
private final GroupManager groupManager;
private final BroadcastService broadcastService;
private String currentUsername;
public MessageHandler(UserManager userManager, OnlineUserManager onlineUserManager,
GroupManager groupManager, BroadcastService broadcastService) {
this.userManager = userManager;
this.onlineUserManager = onlineUserManager;
this.groupManager = groupManager;
this.broadcastService = broadcastService;
}
public void setCurrentUsername(String username) {
this.currentUsername = username;
}
/**
* 处理消息
*/
public void handleMessage(Message message) throws IOException {
switch (message.getType()) {
case LOGIN:
handleLogin(message);
break;
case PRIVATE_CHAT:
handlePrivateChat(message);
break;
case GROUP_CHAT:
handleGroupChat(message);
break;
case FILE_PRIVATE:
handlePrivateFile(message);
break;
case FILE_GROUP:
handleGroupFile(message);
break;
case SHAKE:
handleShake(message);
break;
case CREATE_GROUP:
handleCreateGroup(message);
break;
case SEARCH_GROUP:
handleSearchGroup(message);
break;
case JOIN_GROUP:
handleJoinGroup(message);
break;
case GET_ONLINE_USERS:
handleGetOnlineUsers();
break;
case REGISTER:
handleRegister(message);
break;
case FIND_PASSWORD:
handleFindPassword(message);
break;
case RESET_PASSWORD:
handleResetPassword(message);
break;
default:
break;
}
}
private void handleLogin(Message message) throws IOException {
String username = message.getSender();
String password = message.getPassword();
if (userManager.authenticate(username, password)) {
onlineUserManager.addUser(username,
onlineUserManager.getUserStream(username));
Message response = new Message(Message.Type.LOGIN, "服务器",
username, "登录成功");
broadcastService.sendToUser(username, response);
broadcastService.broadcastOnlineUsers();
} else {
Message response = new Message(Message.Type.LOGIN, "服务器",
username, "登录失败:账号或密码错误");
broadcastService.sendToUser(username, response);
}
}
private void handlePrivateChat(Message message) throws IOException {
broadcastService.sendToUser(message.getReceiver(), message);
}
private void handleGroupChat(Message message) throws IOException {
broadcastService.broadcastToGroup(message.getGroupId(), message);
}
private void handlePrivateFile(Message message) throws IOException {
broadcastService.sendToUser(message.getReceiver(), message);
}
private void handleGroupFile(Message message) throws IOException {
broadcastService.broadcastToGroup(message.getGroupId(), message);
}
private void handleShake(Message message) throws IOException {
broadcastService.sendToUser(message.getReceiver(), message);
}
private void handleCreateGroup(Message message) throws IOException {
Group group = groupManager.createGroup(message.getContent());
broadcastService.broadcastGroupList();
Message response = new Message(Message.Type.CREATE_GROUP, "服务器",
currentUsername, "群组创建成功:" + group.getGroupName());
broadcastService.sendToUser(currentUsername, response);
}
private void handleSearchGroup(Message message) throws IOException {
List<Group> groups = groupManager.searchGroups(message.getContent());
Message response = new Message(Message.Type.SEARCH_GROUP, "服务器",
currentUsername, "");
response.setGroupList(groups);
broadcastService.sendToUser(currentUsername, response);
}
private void handleJoinGroup(Message message) throws IOException {
boolean success = groupManager.joinGroup(message.getGroupId(),
message.getSender());
if (success) {
broadcastService.broadcastGroupList();
Message response = new Message(Message.Type.JOIN_GROUP, "服务器",
currentUsername, "加入群组成功");
broadcastService.sendToUser(currentUsername, response);
} else {
Message response = new Message(Message.Type.JOIN_GROUP, "服务器",
currentUsername, "加入群组失败:群组不存在");
broadcastService.sendToUser(currentUsername, response);
}
}
private void handleGetOnlineUsers() throws IOException {
broadcastService.broadcastOnlineUsers();
}
private void handleRegister(Message message) throws IOException {
String username = message.getSender();
String password = message.getPassword();
if (userManager.register(username, password)) {
Message response = new Message(Message.Type.REGISTER_RESPONSE,
"服务器", username, "注册成功");
broadcastService.sendToUser(username, response);
} else {
Message response = new Message(Message.Type.REGISTER_RESPONSE,
"服务器", username, "注册失败:用户已存在");
broadcastService.sendToUser(username, response);
}
}
private void handleFindPassword(Message message) throws IOException {
String username = message.getSender();
if (userManager.userExists(username)) {
Message response = new Message(Message.Type.FIND_PASSWORD_RESPONSE,
"服务器", username, "账号验证通过,请重置密码");
broadcastService.sendToUser(username, response);
} else {
Message response = new Message(Message.Type.FIND_PASSWORD_RESPONSE,
"服务器", username, "账号不存在");
broadcastService.sendToUser(username, response);
}
}
private void handleResetPassword(Message message) throws IOException {
String username = message.getSender();
String newPassword = message.getPassword();
if (userManager.updatePassword(username, newPassword)) {
Message response = new Message(Message.Type.RESET_PASSWORD_RESPONSE,
"服务器", username, "密码重置成功");
broadcastService.sendToUser(username, response);
} else {
Message response = new Message(Message.Type.RESET_PASSWORD_RESPONSE,
"服务器", username, "密码重置失败:账号不存在");
broadcastService.sendToUser(username, response);
}
}
}
2.6 客户端处理器
创建 server/handlers/ClientHandler.java:
客户端处理器的生命周期图:
sequenceDiagram participant Server as 服务端 participant ClientHandler as ClientHandler participant Client as 客户端 Server->>ClientHandler: 创建ClientHandler实例 ClientHandler->>Client: 建立Socket连接 ClientHandler->>ClientHandler: 初始化IO流 loop 消息处理循环 Client->>ClientHandler: 发送Message对象 ClientHandler->>ClientHandler: 反序列化消息 ClientHandler->>MessageHandler: 调用handleMessage MessageHandler->>Server组件: 执行业务逻辑 Server组件->>ClientHandler: 返回结果 ClientHandler->>Client: 发送响应 end alt 连接断开或异常 ClientHandler->>ClientHandler: 执行cleanup() ClientHandler->>Client: 关闭连接 end
package server.handlers;
import common.Message;
import server.broadcast.BroadcastService;
import server.handlers.MessageHandler;
import server.managers.GroupManager;
import server.managers.OnlineUserManager;
import server.managers.UserManager;
import java.io.*;
import java.net.Socket;
/**
* 客户端连接处理线程
*/
public class ClientHandler extends Thread {
private final Socket socket;
private ObjectInputStream ois;
private ObjectOutputStream oos;
private final MessageHandler messageHandler;
public ClientHandler(Socket socket, UserManager userManager,
OnlineUserManager onlineUserManager,
GroupManager groupManager,
BroadcastService broadcastService) {
this.socket = socket;
this.messageHandler = new MessageHandler(userManager,
onlineUserManager, groupManager, broadcastService);
}
@Override
public void run() {
try {
oos = new ObjectOutputStream(socket.getOutputStream());
ois = new ObjectInputStream(socket.getInputStream());
Message message;
while ((message = (Message) ois.readObject()) != null) {
messageHandler.setCurrentUsername(message.getSender());
messageHandler.handleMessage(message);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
cleanup();
}
}
private void cleanup() {
try {
if (ois != null) ois.close();
if (oos != null) oos.close();
if (socket != null) socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public ObjectOutputStream getOutputStream() {
return oos;
}
}
2.7 服务端主类
创建 server/ChatServer.java:
服务端的整体架构图:
graph TB subgraph "服务端核心" A[ChatServer主类] end subgraph "管理器层" B[UserManager] C[OnlineUserManager] D[GroupManager] end subgraph "服务层" E[BroadcastService] end subgraph "处理器层" F[ClientHandler实例1] G[ClientHandler实例N] end subgraph "消息处理层" H[ServerMessageHandler1] I[ServerMessageHandlerN] end A --> B A --> C A --> D A --> E A --> F A --> G B --> H C --> H D --> H E --> H B --> I C --> I D --> I E --> I style A fill:#e3f2fd style B fill:#f3e5f5 style C fill:#f3e5f5 style D fill:#f3e5f5 style E fill:#e8f5e8 style F fill:#fff3e0 style G fill:#fff3e0
package server;
import server.broadcast.BroadcastService;
import server.handlers.ClientHandler;
import server.managers.GroupManager;
import server.managers.OnlineUserManager;
import server.managers.UserManager;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 聊天室服务端
*/
public class ChatServer {
private static final int PORT = 8888;
private final UserManager userManager;
private final OnlineUserManager onlineUserManager;
private final GroupManager groupManager;
private final BroadcastService broadcastService;
public ChatServer() {
this.userManager = new UserManager();
this.onlineUserManager = new OnlineUserManager();
this.groupManager = new GroupManager();
this.broadcastService = new BroadcastService(onlineUserManager, groupManager);
}
public void start() {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("聊天室服务端已启动,端口:" + PORT);
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端连接:" + clientSocket);
ClientHandler clientHandler = new ClientHandler(
clientSocket,
userManager,
onlineUserManager,
groupManager,
broadcastService
);
onlineUserManager.addUser("temp", clientHandler.getOutputStream());
clientHandler.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new ChatServer().start();
}
}
第三阶段:客户端开发
3.1 网络管理器
创建 client/network/NetworkManager.java:
客户端网络管理器的连接和通信流程图:
flowchart TD A[NetworkManager初始化] --> B[连接到服务器] B --> C{连接是否成功?} C -->|是| D[初始化IO流] C -->|否| E[显示连接失败消息] D --> F[准备发送/接收消息] F --> G[发送Message对象] F --> H[接收Message对象] G --> I[序列化并发送到服务端] H --> J[反序列化从服务端接收] I --> K[等待响应] J --> L[返回Message对象] E --> M[流程结束] L --> N[消息处理] N --> O[UI更新] style A fill:#e3f2fd style C fill:#ffccbc style D fill:#e8f5e8 style E fill:#ffcdd2 style N fill:#f3e5f5
package client.network;
import client.utils.LogUtil;
import common.Message;
import javax.swing.*;
import java.io.*;
import java.net.Socket;
/**
* 网络管理类
* 负责与服务器的网络通信
*/
public class NetworkManager {
private String serverIp;
private static final int SERVER_PORT = 8888;
private Socket socket;
private ObjectOutputStream oos;
private ObjectInputStream ois;
public NetworkManager(String serverIp) {
this.serverIp = serverIp;
}
/**
* 连接到服务器
*/
public boolean connectToServer() {
try {
socket = new Socket(serverIp, SERVER_PORT);
oos = new ObjectOutputStream(socket.getOutputStream());
ois = new ObjectInputStream(socket.getInputStream());
return true;
} catch (IOException e) {
JOptionPane.showMessageDialog(null,
"连接服务器失败:" + e.getMessage());
return false;
}
}
/**
* 发送消息
*/
public void sendMessage(Message message) {
try {
if (oos != null) {
oos.writeObject(message);
oos.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 接收消息
*/
public Message receiveMessage() {
try {
if (ois != null) {
return (Message) ois.readObject();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 重置Socket连接
*/
public void resetSocket() {
try {
if (ois != null) ois.close();
if (oos != null) oos.close();
if (socket != null) socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.2 UI组件类
UI组件工厂
创建 client/UIComponentFactory.java:
UI组件工厂的设计模式图:
classDiagram class UIComponentFactory { +JButton createStyledButton(String text) +JTextField createStyledTextField() +JPanel createRoundedPanel(Color bgColor, int padding) } class RoundBorder { -int radius -Color color +paintBorder(Component c, Graphics g, int x, int y, int width, int height) } UIComponentFactory ..> JButton : creates UIComponentFactory ..> JTextField : creates UIComponentFactory ..> JPanel : creates UIComponentFactory ..> RoundBorder : uses
package client.ui;
import javax.swing.*;
import javax.swing.border.AbstractBorder;
import java.awt.*;
/**
* UI组件工厂类
* 提供统一的UI组件创建方法,保持界面风格一致
*/
public class UIComponentFactory {
/**
* 创建美化样式的按钮
*/
public JButton createStyledButton(String text) {
JButton button = new JButton(text);
button.setFont(new Font("微软雅黑", Font.PLAIN, 14));
button.setBackground(new Color(64, 158, 255));
button.setForeground(Color.WHITE);
button.setBorder(BorderFactory.createEmptyBorder(8, 20, 8, 20));
button.setFocusPainted(false);
button.setBorderPainted(false);
button.setCursor(new Cursor(Cursor.HAND_CURSOR));
button.setUI(new javax.swing.plaf.basic.BasicButtonUI() {
@Override
public void paint(Graphics g, JComponent c) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
g2.setColor(c.getBackground());
g2.fillRoundRect(0, 0, c.getWidth(), c.getHeight(), 8, 8);
super.paint(g2, c);
g2.dispose();
}
});
return button;
}
/**
* 创建美化的输入框
*/
public JTextField createStyledTextField() {
JTextField field = new JTextField();
field.setFont(new Font("微软雅黑", Font.PLAIN, 14));
field.setBorder(new RoundBorder(8, new Color(204, 204, 204)));
field.setBackground(Color.WHITE);
return field;
}
/**
* 创建圆角面板
*/
public JPanel createRoundedPanel(Color bgColor, int padding) {
JPanel panel = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
g2.setColor(getBackground());
g2.fillRoundRect(0, 0, getWidth()-1, getHeight()-1, 12, 12);
g2.dispose();
}
};
panel.setBackground(bgColor);
panel.setBorder(BorderFactory.createEmptyBorder(
padding, padding, padding, padding
));
panel.setOpaque(false);
return panel;
}
}
/**
* 圆角边框类
*/
class RoundBorder extends AbstractBorder {
private int radius;
private Color color;
public RoundBorder(int radius, Color color) {
this.radius = radius;
this.color = color;
}
@Override
public void paintBorder(Component c, Graphics g,
int x, int y, int width, int height) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
g2.setColor(color);
g2.drawRoundRect(x, y, width-1, height-1, radius, radius);
g2.dispose();
}
}
3.3 消息监听器
创建 client/MessageListener.java:
消息监听器的工作流程图:
graph TD A[MessageListener启动] --> B[进入无限循环] B --> C[从网络接收消息] C --> D{消息是否为空?} D -->|否| E[调用MessageHandler处理] D -->|是| B E --> F[UI更新] F --> B style A fill:#e3f2fd style D fill:#ffccbc style E fill:#e8f5e8 style F fill:#f3e5f5
package client;
import client.handler.MessageHandler;
import client.network.NetworkManager;
import common.Message;
import javax.swing.*;
/**
* 消息监听线程
* 负责持续监听服务器发送的消息
*/
public class MessageListener extends Thread {
private final NetworkManager networkManager;
private final MessageHandler messageHandler;
private final JTextArea chatArea;
public MessageListener(NetworkManager networkManager, MessageHandler messageHandler,
JTextArea chatArea) {
this.networkManager = networkManager;
this.messageHandler = messageHandler;
this.chatArea = chatArea;
}
@Override
public void run() {
try {
while (true) {
Message message = networkManager.receiveMessage();
if (message != null) {
messageHandler.handleMessage(message);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.4 消息处理器
创建 client/handler/MessageHandler.java:
客户端消息处理器的处理流程图:
flowchart TD A[MessageHandler.handleMessage()] --> B{判断消息类型} B -->|PRIVATE_CHAT| C[处理私聊消息] B -->|GROUP_CHAT| D[处理群聊消息] B -->|FILE_PRIVATE or FILE_GROUP| E[处理文件消息] B -->|SHAKE| F[处理窗口抖动] B -->|ONLINE_USERS| G[处理在线用户列表] B -->|GROUP_LIST| H[处理群组列表] B -->|ONLINE_NOTIFY| I[处理上线通知] B -->|OFFLINE_NOTIFY| J[处理下线通知] B -->|其他| K[忽略消息] C --> C1[格式化私聊消息] C --> C2[添加到聊天区域] D --> D1[格式化群聊消息] D --> D2[添加到聊天区域] E --> E1[显示文件接收对话框] E --> E2[询问是否保存文件] E --> E3{用户选择} E3 -->|是| E4[保存文件] E3 -->|否| E5[忽略文件] F --> F1[显示抖动通知] F --> F2[触发窗口抖动效果] G --> G1[更新用户列表UI] H --> H1[更新群组列表UI] I --> I1[显示上线通知] J --> J1[显示下线通知] style A fill:#e3f2fd style B fill:#f1f8e9 style C fill:#e8f5e8 style D fill:#e8f5e8 style E fill:#e8f5e8 style E3 fill:#ffccbc
package client.handler;
import client.ChatClient;
import client.ui.ChatMainUI;
import common.Message;
import javax.swing.*;
import java.awt.Point;
/**
* 消息处理器类
* 负责处理从服务器接收到的各种消息类型
*/
public class MessageHandler {
private final ChatClient chatClient;
private final ChatMainUI chatMainUI;
public MessageHandler(ChatClient chatClient, ChatMainUI chatMainUI) {
this.chatClient = chatClient;
this.chatMainUI = chatMainUI;
}
/**
* 处理消息
*/
public void handleMessage(Message message) {
SwingUtilities.invokeLater(() -> {
switch (message.getType()) {
case PRIVATE_CHAT:
handlePrivateChat(message);
break;
case GROUP_CHAT:
handleGroupChat(message);
break;
case FILE_PRIVATE:
case FILE_GROUP:
handleFile(message);
break;
case SHAKE:
handleShake(message);
break;
case ONLINE_USERS:
handleOnlineUsers(message);
break;
case GROUP_LIST:
handleGroupList(message);
break;
case ONLINE_NOTIFY:
handleOnlineNotify(message);
break;
case OFFLINE_NOTIFY:
handleOfflineNotify(message);
break;
default:
break;
}
});
}
private void handlePrivateChat(Message message) {
chatMainUI.getChatArea().append(
"【私聊-" + message.getSender() + "】" +
message.getSender() + ":" + message.getContent() + "\n"
);
}
private void handleGroupChat(Message message) {
chatMainUI.getChatArea().append(
"【群聊-" + message.getGroupId() + "】" +
message.getSender() + ":" + message.getContent() + "\n"
);
}
private void handleFile(Message message) {
String fileName = message.getFileName();
long fileSize = message.getFileSize();
chatMainUI.getChatArea().append(
"【系统消息】收到文件:" + fileName +
"(大小:" + formatFileSize(fileSize) + ")\n"
);
int confirm = JOptionPane.showConfirmDialog(
chatMainUI,
"是否保存文件:" + fileName + "?",
"接收文件",
JOptionPane.YES_NO_OPTION
);
if (confirm == JOptionPane.YES_OPTION) {
chatClient.saveBytesToFile(message.getFileData(), fileName);
}
}
private void handleShake(Message message) {
chatMainUI.getChatArea().append(
"【系统消息】收到来自" + message.getSender() + "的窗口抖动!\n"
);
chatClient.shakeWindow();
}
private void handleOnlineUsers(Message message) {
chatClient.updateUserList(message.getOnlineUsers());
}
private void handleGroupList(Message message) {
chatClient.updateGroupList(message.getGroupList());
}
private void handleOnlineNotify(Message message) {
chatMainUI.getChatArea().append(
"【系统消息】" + message.getSender() + " 上线了\n"
);
}
private void handleOfflineNotify(Message message) {
chatMainUI.getChatArea().append(
"【系统消息】" + message.getSender() + " 下线了\n"
);
}
private void shakeWindow() {
chatClient.shakeWindow();
}
private String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.2f KB", size / 1024.0);
} else {
return String.format("%.2f MB", size / (1024.0 * 1024));
}
}
}
3.5 Manager类
聊天管理器
创建 client/managers/ChatManager.java:
聊天管理器的功能结构图:
graph TD A[ChatManager] --> B[发送消息] A --> C[发送窗口抖动] A --> D[发送创建群聊请求] A --> E[发送搜索群聊请求] A --> F[发送加入群聊请求] B --> B1{判断聊天类型} B1 -->|私聊| B2[创建PRIVATE_CHAT消息] B1 -->|群聊| B3[创建GROUP_CHAT消息] B2 --> B4[发送到服务端] B3 --> B4 C --> C1{判断目标类型} C1 -->|私聊| C2[创建SHAKE消息给用户] C1 -->|群聊| C3[创建SHAKE消息给群组] C2 --> C4[发送到服务端] C3 --> C4 D --> D1[创建CREATE_GROUP消息] D --> D2[发送到服务端] E --> E1[创建SEARCH_GROUP消息] E --> E2[发送到服务端] F --> F1[创建JOIN_GROUP消息] F --> F2[发送到服务端] style A fill:#e3f2fd style B1 fill:#ffccbc style C1 fill:#ffccbc
package client.managers;
import client.ChatClient;
import common.Message;
import javax.swing.*;
/**
* 聊天管理类
* 负责处理聊天消息的发送逻辑
*/
public class ChatManager {
private final ChatClient chatClient;
public ChatManager(ChatClient chatClient) {
this.chatClient = chatClient;
}
/**
* 发送聊天消息
*/
public void sendMessage() {
String content = chatClient.getInputField().getText().trim();
if (content.isEmpty()) {
return;
}
try {
Message.Type type;
String targetName = (String) chatClient.getTargetBox().getSelectedItem();
String target = null;
if (chatClient.getChatTypeBox().getSelectedItem().equals("群聊")) {
type = Message.Type.GROUP_CHAT;
target = (String) chatClient.getTargetBox().getClientProperty("groupId");
if (target == null || target.isEmpty()) {
target = chatClient.getGroupNameToIdMap().get(targetName);
}
if (target == null || target.isEmpty()) {
JOptionPane.showMessageDialog(chatClient, "未找到该群聊的ID,无法发送消息!");
return;
}
} else {
type = Message.Type.PRIVATE_CHAT;
target = targetName;
}
Message chatMsg = new Message(type, chatClient.getUsername(), target, content);
chatClient.getNetworkManager().sendMessage(chatMsg);
chatClient.getInputField().setText("");
} catch (Exception e) {
JOptionPane.showMessageDialog(chatClient, "发送消息失败:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 发送窗口抖动
*/
public void sendShake() {
String targetName = (String) chatClient.getTargetBox().getSelectedItem();
String target = null;
if (chatClient.getChatTypeBox().getSelectedItem().equals("群聊")) {
target = (String) chatClient.getTargetBox().getClientProperty("groupId");
if (target == null || target.isEmpty()) {
target = chatClient.getGroupNameToIdMap().get(targetName);
}
} else {
target = targetName;
}
if (target == null || target.isEmpty()) {
JOptionPane.showMessageDialog(chatClient, "请先选择聊天对象!");
return;
}
try {
Message shakeMsg = new Message(Message.Type.SHAKE, chatClient.getUsername(), target, "");
chatClient.getNetworkManager().sendMessage(shakeMsg);
} catch (Exception e) {
JOptionPane.showMessageDialog(chatClient, "发送抖动失败:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 发送创建群聊请求
*/
public void sendCreateGroupRequest(String groupName) {
try {
Message createMsg = new Message(Message.Type.CREATE_GROUP, chatClient.getUsername(), "", groupName);
chatClient.getNetworkManager().sendMessage(createMsg);
} catch (Exception e) {
JOptionPane.showMessageDialog(chatClient, "发送创建群聊请求失败:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 发送查找群聊请求
*/
public void sendSearchGroupRequest(String keyword) {
try {
Message searchMsg = new Message(Message.Type.SEARCH_GROUP, chatClient.getUsername(), "", keyword);
chatClient.getNetworkManager().sendMessage(searchMsg);
} catch (Exception e) {
JOptionPane.showMessageDialog(chatClient, "发送查找群聊请求失败:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 发送加入群聊请求
*/
public void sendJoinGroupRequest(String groupId) {
try {
Message joinMsg = new Message(Message.Type.JOIN_GROUP, chatClient.getUsername(), groupId, "");
chatClient.getNetworkManager().sendMessage(joinMsg);
} catch (Exception e) {
JOptionPane.showMessageDialog(chatClient, "发送加入群聊请求失败:" + e.getMessage());
e.printStackTrace();
}
}
}
文件管理器
创建 client/managers/FileManager.java:
文件管理器的操作流程图:
graph TD A[FileManager] --> B[选择文件] A --> C[发送文件] A --> D[截图并发送] B --> B1[打开文件选择对话框] B1 --> B2[用户选择文件] B2 --> B3[显示文件信息] C --> C1{是否有选中文件?} C1 -->|否| C2[提示选择文件] C1 -->|是| C3{目标是否存在?} C3 -->|否| C4[提示选择目标] C3 -->|是| C5[读取文件内容] C5 --> C6[创建文件消息] C6 --> C7[发送到服务端] D --> D1[捕获屏幕截图] D1 --> D2[保存为临时文件] D2 --> D3[询问是否发送] D3 --> D4{用户确认?} D4 -->|是| D5[设置为选中文件] D4 -->|否| D6[删除临时文件] D5 --> C style A fill:#e3f2fd style C1 fill:#ffccbc style C3 fill:#ffccbc style D3 fill:#ffccbc style D4 fill:#ffccbc
package client.managers;
import client.ChatClient;
import common.Message;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* 文件管理类
* 负责处理文件选择、发送和截图功能
*/
public class FileManager {
private final ChatClient chatClient;
private File selectedFile;
public FileManager(ChatClient chatClient) {
this.chatClient = chatClient;
}
public File getSelectedFile() {
return selectedFile;
}
public void setSelectedFile(File file) {
this.selectedFile = file;
}
/**
* 选择文件
*/
public void selectFile() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setDialogTitle("选择要发送的文件");
int result = fileChooser.showOpenDialog(chatClient);
if (result == JFileChooser.APPROVE_OPTION) {
selectedFile = fileChooser.getSelectedFile();
long fileSize = selectedFile.length();
String fileSizeStr = formatFileSize(fileSize);
JOptionPane.showMessageDialog(chatClient,
"已选择文件:" + selectedFile.getName() + "\n文件大小:" + fileSizeStr);
}
}
/**
* 发送文件
*/
public void sendFile() {
if (selectedFile == null || !selectedFile.exists()) {
JOptionPane.showMessageDialog(chatClient, "请先选择文件!");
return;
}
String targetName = (String) chatClient.getTargetBox().getSelectedItem();
String target = null;
Message.Type type;
if (chatClient.getChatTypeBox().getSelectedItem().equals("群聊")) {
type = Message.Type.FILE_GROUP;
target = (String) chatClient.getTargetBox().getClientProperty("groupId");
if (target == null || target.isEmpty()) {
target = chatClient.getGroupNameToIdMap().get(targetName);
}
} else {
type = Message.Type.FILE_PRIVATE;
target = targetName;
}
if (target == null || target.isEmpty()) {
JOptionPane.showMessageDialog(chatClient, "请先选择聊天对象!");
return;
}
try {
FileInputStream fis = new FileInputStream(selectedFile);
byte[] fileData = new byte[(int) selectedFile.length()];
fis.read(fileData);
fis.close();
Message fileMsg = new Message(type, chatClient.getUsername(), target,
selectedFile.getName(), selectedFile.length(), fileData);
chatClient.getNetworkManager().sendMessage(fileMsg);
chatClient.getChatArea().append("【系统消息】文件[" + selectedFile.getName() + "]发送成功\n");
selectedFile = null;
} catch (IOException e) {
JOptionPane.showMessageDialog(chatClient, "发送文件失败:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 截图并发送
*/
public void captureAndSendScreenshot() {
try {
Robot robot = new Robot();
Rectangle screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
BufferedImage screenshot = robot.createScreenCapture(screenRect);
File screenshotFile = new File("screenshot_" + System.currentTimeMillis() + ".png");
javax.imageio.ImageIO.write(screenshot, "png", screenshotFile);
int confirm = JOptionPane.showConfirmDialog(
chatClient,
"截图已生成,是否发送给当前选中的" +
(chatClient.getChatTypeBox().getSelectedItem().equals("群聊") ? "群聊" : "好友") + "?",
"确认发送截图",
JOptionPane.YES_NO_OPTION
);
if (confirm != JOptionPane.YES_OPTION) {
screenshotFile.delete();
return;
}
selectedFile = screenshotFile;
sendFile();
new Thread(() -> {
try {
Thread.sleep(1000);
if (screenshotFile.exists()) {
screenshotFile.delete();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}).start();
} catch (Exception ex) {
JOptionPane.showMessageDialog(chatClient, "截图发送失败:" + ex.getMessage());
ex.printStackTrace();
}
}
/**
* 格式化文件大小
*/
private String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.2f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.2f MB", size / (1024.0 * 1024));
} else {
return String.format("%.2f GB", size / (1024.0 * 1024 * 1024));
}
}
}
数据管理器
创建 client/managers/DataManager.java:
数据管理器的数据流图:
graph LR A[DataManager] --> B[更新在线用户列表] A --> C[更新群列表] A --> D[保存文件到本地] B --> B1[获取在线用户数据] B1 --> B2[清空现有列表模型] B2 --> B3[添加新用户到模型] B3 --> B4[更新UI显示] C --> C1[获取群组列表数据] C1 --> C2[清空现有群组模型] C2 --> C3[添加新群组到模型] C3 --> C4[更新群组ID-名称映射] C4 --> C5[更新UI显示] D --> D1[获取文件数据和名称] D1 --> D2[打开保存文件对话框] D2 --> D3{用户确认保存?} D3 -->|是| D4[写入文件到磁盘] D3 -->|否| D5[取消保存] D4 --> D6[显示保存成功消息] D5 --> D7[显示取消保存消息] style A fill:#e3f2fd style B3 fill:#e8f5e8 style C3 fill:#e8f5e8 style D3 fill:#ffccbc
package client.managers;
import client.ChatClient;
import common.Group;
import javax.swing.*;
import java.util.List;
/**
* 数据管理类
* 负责更新用户列表、群列表等数据
*/
public class DataManager {
private final ChatClient chatClient;
public DataManager(ChatClient chatClient) {
this.chatClient = chatClient;
}
/**
* 更新在线用户列表UI
*/
public void updateUserList(List<String> onlineUsers) {
SwingUtilities.invokeLater(() -> {
chatClient.getUserListModel().clear();
for (String user : onlineUsers) {
chatClient.getUserListModel().addElement(user);
}
});
}
/**
* 更新群列表
*/
public void updateGroupList(List<Group> groupList) {
SwingUtilities.invokeLater(() -> {
chatClient.getGroupListModel().clear();
chatClient.getGroupIdToNameMap().clear();
chatClient.getGroupNameToIdMap().clear();
for (Group group : groupList) {
chatClient.getGroupListModel().addElement(group.getGroupName());
chatClient.getGroupIdToNameMap().put(group.getGroupId(), group.getGroupName());
chatClient.getGroupNameToIdMap().put(group.getGroupName(), group.getGroupId());
}
if (chatClient.getChatTypeBox().getSelectedItem().equals("群聊")) {
chatClient.getTargetBox().removeAllItems();
for (int i = 0; i < chatClient.getGroupListModel().size(); i++) {
chatClient.getTargetBox().addItem(chatClient.getGroupListModel().getElementAt(i));
}
}
});
}
/**
* 保存字节数组为文件
*/
public void saveBytesToFile(byte[] data, String fileName) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setSelectedFile(new java.io.File(fileName));
int result = fileChooser.showSaveDialog(chatClient);
if (result == JFileChooser.APPROVE_OPTION) {
java.io.File saveFile = fileChooser.getSelectedFile();
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(saveFile)) {
fos.write(data);
chatClient.getChatArea().append("【系统消息】已接收文件:" + fileName + ",保存至:" + saveFile.getAbsolutePath() + "\n");
} catch (java.io.IOException e) {
chatClient.getChatArea().append("【系统消息】文件保存失败:" + e.getMessage() + "\n");
e.printStackTrace();
}
} else {
chatClient.getChatArea().append("【系统消息】取消保存文件:" + fileName + "\n");
}
}
}
窗口管理器
创建 client/managers/WindowManager.java:
窗口管理器的抖动效果实现图:
graph TD A[WindowManager.shakeWindow()] --> B{是否已在抖动?} B -->|是| C[直接返回] B -->|否| D[设置抖动状态为true] D --> E[保存原始位置] E --> F[启动抖动定时器] F --> G[随机改变窗口位置] G --> H[启动恢复定时器] H --> I[恢复原始位置] I --> J[设置抖动状态为false]
package client.managers;
import client.ChatClient;
import javax.swing.*;
import java.awt.*;
/**
* 窗口管理类
* 负责窗口抖动等窗口相关功能
*/
public class WindowManager {
private final ChatClient chatClient;
private Point originalLocation;
private boolean isShaking = false;
public WindowManager(ChatClient chatClient) {
this.chatClient = chatClient;
this.originalLocation = chatClient.getLocation();
}
/**
* 执行窗口抖动
*/
public void shakeWindow() {
if (isShaking) {
return;
}
isShaking = true;
originalLocation = chatClient.getLocation();
new Timer(10, e -> {
int offsetX = (int) (Math.random() * 20 - 10);
int offsetY = (int) (Math.random() * 20 - 10);
chatClient.setLocation(originalLocation.x + offsetX, originalLocation.y + offsetY);
}).start();
new Timer(500, e -> {
chatClient.setLocation(originalLocation);
isShaking = false;
}).start();
}
/**
* 检查是否正在抖动
*/
public boolean isShaking() {
return isShaking;
}
/**
* 设置抖动状态
*/
public void setShaking(boolean shaking) {
isShaking = shaking;
}
}
认证管理器
创建 client/managers/AuthenticationManager.java:
认证管理器的认证流程图:
graph TD A[AuthenticationManager] --> B[用户注册] A --> C[用户登录] A --> D[找回密码] A --> E[重置密码] B --> B1[验证输入] B1 --> B2[创建注册消息] B2 --> B3[发送到服务端] B3 --> B4[接收响应] B4 --> B5{注册成功?} B5 -->|是| B6[关闭注册窗口] B5 -->|否| B7[显示错误信息] C --> C1[验证输入] C1 --> C2[连接到服务器] C2 --> C3[创建登录消息] C3 --> C4[发送到服务端] C4 --> C5[接收响应] C5 --> C6{登录成功?} C6 -->|是| C7[保存用户信息] C6 -->|否| C8[清除密码字段] D --> D1[获取账号] D1 --> D2[创建找回密码消息] D2 --> D3[发送到服务端] D3 --> D4[接收响应] D4 --> D5{验证通过?} D5 -->|是| E D5 -->|否| D6[显示错误信息] E --> E1[获取新密码] E1 --> E2[验证密码强度] E2 --> E3[创建重置密码消息] E3 --> E4[发送到服务端] E4 --> E5[接收响应] E5 --> E6{重置成功?} E6 -->|是| E7[清除原密码字段] E6 -->|否| E8[显示错误信息] style A fill:#e3f2fd style B5 fill:#ffccbc style C6 fill:#ffccbc style D5 fill:#ffccbc style E6 fill:#ffccbc
package client.managers;
import client.ChatClient;
import common.Message;
import javax.swing.*;
/**
* 认证管理类
* 负责用户注册、登录、密码找回等功能
*/
public class AuthenticationManager {
private final ChatClient chatClient;
public AuthenticationManager(ChatClient chatClient) {
this.chatClient = chatClient;
}
/**
* 用户注册
*/
public void register(String account, String password) {
try {
Message registerMsg = new Message(Message.Type.REGISTER, account, "");
registerMsg.setPassword(password);
chatClient.getNetworkManager().sendMessage(registerMsg);
Message response = chatClient.getNetworkManager().receiveMessage();
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), response.getContent());
if (response.getContent().contains("成功")) {
chatClient.getLoginRegisterFrame().dispose();
}
} catch (Exception ex) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "注册失败:" + ex.getMessage());
ex.printStackTrace();
}
}
/**
* 用户登录
*/
public void login(String account, String password, String serverIp) {
try {
chatClient.setServerIp(serverIp);
if (!chatClient.getNetworkManager().connectToServer()) {
return;
}
Message loginMsg = new Message(Message.Type.LOGIN, account, "");
loginMsg.setPassword(password);
chatClient.getNetworkManager().sendMessage(loginMsg);
Message response = chatClient.getNetworkManager().receiveMessage();
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), response.getContent());
if (response.getContent().contains("成功")) {
chatClient.setUsername(account);
chatClient.getLoginRegisterFrame().dispose();
chatClient.initChatUI();
chatClient.startMessageListener();
Message getUsersMsg = new Message(Message.Type.GET_ONLINE_USERS, account, "", "");
chatClient.getNetworkManager().sendMessage(getUsersMsg);
} else {
chatClient.getLoginPwdField().setText("");
}
} catch (Exception ex) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "登录失败:" + ex.getMessage());
ex.printStackTrace();
chatClient.resetSocket();
System.exit(1);
}
}
/**
* 找回密码
*/
public void findPassword(String account) {
try {
Message findMsg = new Message(Message.Type.FIND_PASSWORD, account, "");
chatClient.getNetworkManager().sendMessage(findMsg);
Message response = chatClient.getNetworkManager().receiveMessage();
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), response.getContent());
if (response.getContent().contains("验证通过")) {
resetPassword(account);
}
} catch (Exception ex) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "验证账号失败:" + ex.getMessage());
chatClient.resetSocket();
ex.printStackTrace();
}
}
/**
* 重置密码
*/
public void resetPassword(String account) {
JPasswordField newPwdField = new JPasswordField();
int result = JOptionPane.showConfirmDialog(
chatClient.getLoginRegisterFrame(),
new Object[]{"请输入新密码:", newPwdField},
"重置密码",
JOptionPane.OK_CANCEL_OPTION
);
if (result == JOptionPane.OK_OPTION) {
String newPassword = new String(newPwdField.getPassword()).trim();
if (newPassword.isEmpty()) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "新密码不能为空!");
return;
}
try {
Message resetMsg = new Message(Message.Type.RESET_PASSWORD, account, "");
resetMsg.setPassword(newPassword);
chatClient.getNetworkManager().sendMessage(resetMsg);
Message response = chatClient.getNetworkManager().receiveMessage();
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), response.getContent());
if (response.getContent().contains("成功")) {
chatClient.getLoginPwdField().setText("");
}
} catch (Exception ex) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "重置密码失败:" + ex.getMessage());
ex.printStackTrace();
}
}
}
}
3.6 客户端主类
创建 client/ChatClient.java:
package client;
import client.managers.ChatManager;
import client.managers.DataManager;
import client.managers.FileManager;
import client.managers.WindowManager;
import client.network.NetworkManager;
import client.ui.ChatMainUI;
import client.ui.LoginRegisterUI;
import client.ui.UIComponentFactory;
import client.handler.MessageHandler;
import common.Group;
import common.Message;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* 聊天室客户端
*/
public class ChatClient extends JFrame {
private String serverIp = "127.0.0.1";
private String username;
private NetworkManager networkManager;
private MessageHandler messageHandler;
private UIComponentFactory uiComponentFactory;
private ChatManager chatManager;
private FileManager fileManager;
private DataManager dataManager;
private WindowManager windowManager;
private JTextArea chatArea;
private JTextField inputField;
private JComboBox<String> chatTypeBox;
private JComboBox<String> targetBox;
private DefaultListModel<String> userListModel;
private DefaultListModel<String> groupListModel;
private Map<String, String> groupIdToNameMap;
private Map<String, String> groupNameToIdMap;
private JFrame loginRegisterFrame;
private JTextField loginAccountField;
private JPasswordField loginPwdField;
private JTextField registerAccountField;
private JPasswordField registerPwdField;
public ChatClient() {
uiComponentFactory = new UIComponentFactory();
showLoginRegisterFrame();
}
public void login(String account, String password, String serverIp) {
this.serverIp = serverIp;
login(account, password);
}
public void login(String account, String password) {
networkManager = new NetworkManager(serverIp);
if (!networkManager.connectToServer()) {
return;
}
Message loginMsg = new Message(Message.Type.LOGIN, account, "");
loginMsg.setPassword(password);
networkManager.sendMessage(loginMsg);
Message response = networkManager.receiveMessage();
if (response != null && response.getContent().contains("成功")) {
this.username = account;
SwingUtilities.invokeLater(() -> {
loginRegisterFrame.dispose();
initChatUI();
startMessageListener();
});
} else {
JOptionPane.showMessageDialog(loginRegisterFrame, response.getContent());
loginPwdField.setText("");
}
}
public void initChatUI() {
chatMainUI = new ChatMainUI(this);
messageHandler = new MessageHandler(this, chatMainUI);
chatMainUI.initChatUI(username);
chatArea = chatMainUI.getChatArea();
inputField = chatMainUI.getInputField();
chatTypeBox = chatMainUI.getChatTypeBox();
targetBox = chatMainUI.getTargetBox();
userListModel = chatMainUI.getUserListModel();
groupListModel = chatMainUI.getGroupListModel();
groupIdToNameMap = new java.util.HashMap<>();
groupNameToIdMap = new java.util.HashMap<>();
chatManager = new ChatManager(this);
fileManager = new FileManager(this);
dataManager = new DataManager(this);
windowManager = new WindowManager(this);
setTitle("Java Socket 聊天室(账号:" + username + ")");
setSize(800, 500);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
}
public void startMessageListener() {
MessageListener listener = new MessageListener(networkManager, messageHandler, chatArea);
listener.start();
}
public void sendMessage() {
chatManager.sendMessage();
}
public void selectFile() {
fileManager.selectFile();
}
public void sendFile() {
fileManager.sendFile();
}
public void saveBytesToFile(byte[] data, String fileName) {
dataManager.saveBytesToFile(data, fileName);
}
public void updateUserList(List<String> onlineUsers) {
dataManager.updateUserList(onlineUsers);
}
public void updateGroupList(List<Group> groupList) {
dataManager.updateGroupList(groupList);
}
public void sendShakeMessage() {
chatManager.sendShake();
}
public void shakeWindow() {
windowManager.shakeWindow();
}
public void resetSocket() {
if (networkManager != null) {
networkManager.resetSocket();
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new ChatClient();
}
});
}
// Getter 方法
public Map<String, String> getGroupIdToNameMap() {
return groupIdToNameMap;
}
public Map<String, String> getGroupNameToIdMap() {
return groupNameToIdMap;
}
public JComboBox<String> getChatTypeBox() {
return chatTypeBox;
}
public JComboBox<String> getTargetBox() {
return targetBox;
}
public JTextArea getChatArea() {
return chatArea;
}
public JTextField getInputField() {
return inputField;
}
public DefaultListModel<String> getUserListModel() {
return userListModel;
}
public DefaultListModel<String> getGroupListModel() {
return groupListModel;
}
public String getUsername() {
return username;
}
public NetworkManager getNetworkManager() {
return networkManager;
}
public void setUsername(String username) {
this.username = username;
}
public JFrame getLoginRegisterFrame() {
return loginRegisterFrame;
}
public JPasswordField getLoginPwdField() {
return loginPwdField;
}
public JTextField getRegisterAccountField() {
return registerAccountField;
}
public JPasswordField getRegisterPwdField() {
return registerPwdField;
}
public void setServerIp(String serverIp) {
this.serverIp = serverIp;
}
}
第四阶段:运行和测试
4.1 启动服务端
# 编译服务端
javac -d bin -encoding UTF-8 src/common/*.java src/server/**/*.java
# 运行服务端
java -cp bin server.ChatServer
4.2 启动客户端
# 编译客户端
javac -d bin -encoding UTF-8 src/common/*.java src/client/**/*.java
# 运行客户端
java -cp bin client.ChatClient
4.3 测试功能
- 注册新用户:在登录界面点击"注册"按钮
- 登录系统:输入账号密码登录
- 发送私聊消息:选择私聊模式,选择目标用户,发送消息
- 创建群聊:点击"创建群聊"按钮,输入群名
- 加入群聊:在群列表中选择群聊加入
- 发送群聊消息:选择群聊模式,选择群聊,发送消息
- 发送文件:点击"选择文件"按钮,选择文件发送
- 窗口抖动:点击"窗口抖动"按钮发送抖动
- 截图功能:使用截图功能发送屏幕截图
项目特点
代码结构清晰
- 采用分层架构,职责分离
- Manager类负责业务逻辑
- Handler类负责消息处理
- UI类负责界面展示
可扩展性强
- 消息类型使用枚举,易于扩展
- Manager类独立,便于添加新功能
- 网络通信封装,便于更换协议
线程安全
- 使用ConcurrentHashMap保证并发安全
- Swing组件更新在事件调度线程中执行
- 消息监听独立线程运行
用户体验
- 美化的UI界面
- 圆角按钮和面板
- 实时消息更新
- 文件传输进度提示
第五阶段:UI界面开发
5.1 登录注册界面
创建 client/ui/LoginRegisterUI.java:
package client.ui;
import client.ChatClient;
import client.managers.AuthenticationManager;
import javax.swing.*;
import java.awt.*;
/**
* 登录注册界面
*/
public class LoginRegisterUI {
private JFrame frame;
private JTextField loginAccountField;
private JPasswordField loginPwdField;
private JTextField registerAccountField;
private JPasswordField registerPwdField;
private JTextField serverIpField;
private AuthenticationManager authManager;
public LoginRegisterUI(ChatClient chatClient) {
this.authManager = new AuthenticationManager(chatClient);
initializeComponents();
}
private void initializeComponents() {
frame = new JFrame("Java Socket 聊天室 - 登录/注册");
frame.setSize(400, 350);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
// 主面板
JPanel mainPanel = new JPanel(new CardLayout());
// 登录面板
JPanel loginPanel = createLoginPanel();
// 注册面板
JPanel registerPanel = createRegisterPanel();
mainPanel.add(loginPanel, "login");
mainPanel.add(registerPanel, "register");
frame.add(mainPanel, BorderLayout.CENTER);
// 底部服务器IP设置面板
JPanel bottomPanel = createBottomPanel();
frame.add(bottomPanel, BorderLayout.SOUTH);
frame.setLocationRelativeTo(null);
}
private JPanel createLoginPanel() {
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createTitledBorder("登录"));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
// 账号
gbc.gridx = 0; gbc.gridy = 0;
panel.add(new JLabel("账号:"), gbc);
gbc.gridx = 1;
loginAccountField = new JTextField(15);
panel.add(loginAccountField, gbc);
// 密码
gbc.gridx = 0; gbc.gridy = 1;
panel.add(new JLabel("密码:"), gbc);
gbc.gridx = 1;
loginPwdField = new JPasswordField(15);
panel.add(loginPwdField, gbc);
// 登录按钮
gbc.gridx = 0; gbc.gridy = 2;
gbc.gridwidth = 2;
JButton loginBtn = new JButton("登录");
loginBtn.addActionListener(e -> performLogin());
panel.add(loginBtn, gbc);
// 注册链接
gbc.gridy = 3;
JButton registerLink = new JButton("没有账号?点击注册");
registerLink.addActionListener(e -> showRegisterPanel());
panel.add(registerLink, gbc);
return panel;
}
private JPanel createRegisterPanel() {
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createTitledBorder("注册"));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
// 账号
gbc.gridx = 0; gbc.gridy = 0;
panel.add(new JLabel("账号:"), gbc);
gbc.gridx = 1;
registerAccountField = new JTextField(15);
panel.add(registerAccountField, gbc);
// 密码
gbc.gridx = 0; gbc.gridy = 1;
panel.add(new JLabel("密码:"), gbc);
gbc.gridx = 1;
registerPwdField = new JPasswordField(15);
panel.add(registerPwdField, gbc);
// 注册按钮
gbc.gridx = 0; gbc.gridy = 2;
gbc.gridwidth = 2;
JButton registerBtn = new JButton("注册");
registerBtn.addActionListener(e -> performRegister());
panel.add(registerBtn, gbc);
// 返回登录链接
gbc.gridy = 3;
JButton loginLink = new JButton("已有账号?返回登录");
loginLink.addActionListener(e -> showLoginPanel());
panel.add(loginLink, gbc);
return panel;
}
private JPanel createBottomPanel() {
JPanel panel = new JPanel(new FlowLayout());
panel.add(new JLabel("服务器IP:"));
serverIpField = new JTextField("127.0.0.1", 12);
panel.add(serverIpField);
JButton findPwdBtn = new JButton("找回密码");
findPwdBtn.addActionListener(e -> performFindPassword());
panel.add(findPwdBtn);
return panel;
}
private void performLogin() {
String account = loginAccountField.getText().trim();
String password = new String(loginPwdField.getPassword()).trim();
String serverIp = serverIpField.getText().trim();
if (account.isEmpty() || password.isEmpty()) {
JOptionPane.showMessageDialog(frame, "账号和密码不能为空!");
return;
}
if (serverIp.isEmpty()) {
serverIp = "127.0.0.1";
}
authManager.login(account, password, serverIp);
}
private void performRegister() {
String account = registerAccountField.getText().trim();
String password = new String(registerPwdField.getPassword()).trim();
if (account.isEmpty() || password.isEmpty()) {
JOptionPane.showMessageDialog(frame, "账号和密码不能为空!");
return;
}
if (password.length() < 6) {
JOptionPane.showMessageDialog(frame, "密码长度不能少于6位!");
return;
}
authManager.register(account, password);
}
private void performFindPassword() {
String account = JOptionPane.showInputDialog(frame, "请输入要找回密码的账号:");
if (account != null && !account.trim().isEmpty()) {
authManager.findPassword(account.trim());
}
}
private void showRegisterPanel() {
CardLayout cl = (CardLayout) frame.getContentPane().getLayout();
cl.show(frame.getContentPane(), "register");
}
private void showLoginPanel() {
CardLayout cl = (CardLayout) frame.getContentPane().getLayout();
cl.show(frame.getContentPane(), "login");
}
public JFrame getFrame() {
return frame;
}
public JTextField getLoginAccountField() {
return loginAccountField;
}
public JPasswordField getLoginPwdField() {
return loginPwdField;
}
public JTextField getRegisterAccountField() {
return registerAccountField;
}
public JPasswordField getRegisterPwdField() {
return registerPwdField;
}
public JTextField getServerIpField() {
return serverIpField;
}
}
5.2 聊天主界面
创建 client/ui/ChatMainUI.java:
package client.ui;
import client.ChatClient;
import client.managers.ChatManager;
import client.managers.FileManager;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* 聊天主界面
*/
public class ChatMainUI extends JFrame {
private ChatClient chatClient;
private ChatManager chatManager;
private FileManager fileManager;
// 聊天区域
private JTextArea chatArea;
private JTextField inputField;
private JButton sendBtn;
private JButton fileBtn;
private JButton shakeBtn;
private JButton screenshotBtn;
private JButton createGroupBtn;
// 控制面板
private JComboBox<String> chatTypeBox;
private JComboBox<String> targetBox;
private DefaultListModel<String> userListModel;
private JList<String> userList;
private DefaultListModel<String> groupListModel;
private JList<String> groupList;
// 字体选择
private JComboBox<String> fontCombo;
private JComboBox<Integer> fontSizeCombo;
public ChatMainUI(ChatClient chatClient) {
this.chatClient = chatClient;
this.chatManager = new ChatManager(chatClient);
this.fileManager = new FileManager(chatClient);
initializeUI();
}
private void initializeUI() {
setTitle("Java Socket 聊天室");
setSize(900, 600);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
// 创建主面板
JPanel mainPanel = new JPanel(new BorderLayout());
// 顶部控制面板
JPanel topPanel = createTopControlPanel();
mainPanel.add(topPanel, BorderLayout.NORTH);
// 中间聊天显示区域
JPanel centerPanel = createCenterChatPanel();
mainPanel.add(centerPanel, BorderLayout.CENTER);
// 底部输入区域
JPanel bottomPanel = createBottomInputPanel();
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
// 右侧用户列表
JPanel rightPanel = createUserListGroupPanel();
mainPanel.add(rightPanel, BorderLayout.EAST);
add(mainPanel);
}
private JPanel createTopControlPanel() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
panel.add(new JLabel("聊天类型:"));
chatTypeBox = new JComboBox<>(new String[]{"私聊", "群聊"});
chatTypeBox.addActionListener(e -> onChatTypeChanged());
panel.add(chatTypeBox);
panel.add(new JLabel("目标:"));
targetBox = new JComboBox<>();
panel.add(targetBox);
panel.add(new JLabel("字体:"));
fontCombo = new JComboBox<>(new String[]{"微软雅黑", "宋体", "黑体", "Arial", "Times New Roman"});
fontCombo.addActionListener(e -> onChangeFont());
panel.add(fontCombo);
panel.add(new JLabel("字号:"));
fontSizeCombo = new JComboBox<>(new Integer[]{12, 14, 16, 18, 20, 22});
fontSizeCombo.setSelectedItem(14);
fontSizeCombo.addActionListener(e -> onChangeFontSize());
panel.add(fontSizeCombo);
return panel;
}
private JPanel createCenterChatPanel() {
JPanel panel = new JPanel(new BorderLayout());
chatArea = new JTextArea();
chatArea.setEditable(false);
chatArea.setFont(new Font((String)fontCombo.getSelectedItem(), Font.PLAIN, (Integer)fontSizeCombo.getSelectedItem()));
JScrollPane scrollPane = new JScrollPane(chatArea);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
panel.add(scrollPane, BorderLayout.CENTER);
return panel;
}
private JPanel createBottomInputPanel() {
JPanel panel = new JPanel(new BorderLayout());
inputField = new JTextField();
inputField.addActionListener(e -> chatManager.sendMessage());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
sendBtn = new JButton("发送");
sendBtn.addActionListener(e -> chatManager.sendMessage());
buttonPanel.add(sendBtn);
fileBtn = new JButton("发送文件");
fileBtn.addActionListener(e -> fileManager.selectFile());
buttonPanel.add(fileBtn);
shakeBtn = new JButton("窗口抖动");
shakeBtn.addActionListener(e -> chatManager.sendShake());
buttonPanel.add(shakeBtn);
screenshotBtn = new JButton("发送截图");
screenshotBtn.addActionListener(e -> fileManager.captureAndSendScreenshot());
buttonPanel.add(screenshotBtn);
createGroupBtn = new JButton("创建群聊");
createGroupBtn.addActionListener(e -> showCreateGroupDialog());
buttonPanel.add(createGroupBtn);
panel.add(inputField, BorderLayout.CENTER);
panel.add(buttonPanel, BorderLayout.SOUTH);
return panel;
}
private JPanel createUserListGroupPanel() {
JPanel panel = new JPanel(new BorderLayout());
panel.setPreferredSize(new Dimension(200, 0));
// 创建选项卡面板
JTabbedPane tabbedPane = new JTabbedPane();
// 在线用户列表
userListModel = new DefaultListModel<>();
userList = new JList<>(userListModel);
userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
userList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
String selectedUser = userList.getSelectedValue();
if (selectedUser != null && !selectedUser.equals(chatClient.getUsername())) {
targetBox.removeAllItems();
targetBox.addItem(selectedUser);
targetBox.setSelectedItem(selectedUser);
}
}
});
JScrollPane userScrollPane = new JScrollPane(userList);
tabbedPane.addTab("在线用户", userScrollPane);
// 群组列表
groupListModel = new DefaultListModel<>();
groupList = new JList<>(groupListModel);
groupList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
groupList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) {
String selectedGroup = groupList.getSelectedValue();
if (selectedGroup != null) {
targetBox.removeAllItems();
targetBox.addItem(selectedGroup);
targetBox.setSelectedItem(selectedGroup);
}
}
});
JScrollPane groupScrollPane = new JScrollPane(groupList);
tabbedPane.addTab("群组", groupScrollPane);
panel.add(tabbedPane, BorderLayout.CENTER);
return panel;
}
private void onChatTypeChanged() {
String selectedType = (String) chatTypeBox.getSelectedItem();
targetBox.removeAllItems();
if ("私聊".equals(selectedType)) {
// 填充在线用户列表
for (int i = 0; i < userListModel.getSize(); i++) {
String user = userListModel.getElementAt(i);
if (!user.equals(chatClient.getUsername())) {
targetBox.addItem(user);
}
}
} else {
// 填充群组列表
for (int i = 0; i < groupListModel.getSize(); i++) {
targetBox.addItem(groupListModel.getElementAt(i));
}
}
}
private void onChangeFont() {
Font currentFont = chatArea.getFont();
chatArea.setFont(new Font((String)fontCombo.getSelectedItem(), currentFont.getStyle(), currentFont.getSize()));
}
private void onChangeFontSize() {
Font currentFont = chatArea.getFont();
chatArea.setFont(new Font(currentFont.getName(), currentFont.getStyle(), (Integer)fontSizeCombo.getSelectedItem()));
}
private void showCreateGroupDialog() {
String groupName = JOptionPane.showInputDialog(this, "请输入群组名称:");
if (groupName != null && !groupName.trim().isEmpty()) {
chatManager.sendCreateGroupRequest(groupName.trim());
}
}
public void initChatUI(String username) {
// 初始化界面组件
setTitle("Java Socket 聊天室(账号:" + username + ")");
// 默认选择私聊
chatTypeBox.setSelectedIndex(0);
onChatTypeChanged();
// 添加欢迎信息
chatArea.append("=== 欢迎使用Java Socket 聊天室 ===\n");
chatArea.append("当前用户:" + username + "\n");
chatArea.append("时间:" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()) + "\n\n");
}
// Getter 方法
public JTextArea getChatArea() {
return chatArea;
}
public JTextField getInputField() {
return inputField;
}
public JComboBox<String> getChatTypeBox() {
return chatTypeBox;
}
public JComboBox<String> getTargetBox() {
return targetBox;
}
public DefaultListModel<String> getUserListModel() {
return userListModel;
}
public DefaultListModel<String> getGroupListModel() {
return groupListModel;
}
public JList<String> getUserList() {
return userList;
}
public JList<String> getGroupList() {
return groupList;
}
}
5.3 截图管理器
创建 client/ScreenshotManager.java:
package client;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
/**
* 截图管理类
* 负责屏幕截图功能
*/
public class ScreenshotManager {
private ChatClient chatClient;
public ScreenshotManager(ChatClient chatClient) {
this.chatClient = chatClient;
}
/**
* 执行截图操作
*/
public void captureScreenshot() {
try {
Robot robot = new Robot();
Rectangle screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
BufferedImage screenFullImage = robot.createScreenCapture(screenRect);
String fileName = "screenshot_" + System.currentTimeMillis() + ".png";
File screenshotFile = new File(fileName);
ImageIO.write(screenFullImage, "png", screenshotFile);
int confirm = JOptionPane.showConfirmDialog(
chatClient,
"截图已保存到:" + screenshotFile.getAbsolutePath() + "\n是否立即发送给当前选中的联系人?",
"截图完成",
JOptionPane.YES_NO_OPTION
);
if (confirm == JOptionPane.YES_OPTION) {
// 将截图作为文件发送
chatClient.selectFile();
}
} catch (AWTException | IOException e) {
JOptionPane.showMessageDialog(chatClient, "截图失败:" + e.getMessage());
e.printStackTrace();
}
}
/**
* 延迟截图(延迟3秒)
*/
public void delayedCapture() {
JOptionPane.showMessageDialog(chatClient, "将在3秒后进行截图,请准备...");
new Thread(() -> {
try {
Thread.sleep(3000); // 等待3秒
SwingUtilities.invokeLater(() -> {
captureScreenshot();
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
第六阶段:高级功能实现
6.1 错误处理和日志记录
在实际应用中,错误处理和日志记录是非常重要的。我们需要为聊天室应用添加完善的错误处理机制:
添加日志工具类
创建 client/utils/LogUtil.java:
package client.utils;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 日志工具类
*/
public class LogUtil {
private static final String LOG_FILE_PATH = "chatroom.log";
public static void info(String message) {
log("INFO", message);
}
public static void warning(String message) {
log("WARNING", message);
}
public static void error(String message, Exception e) {
log("ERROR", message + " - " + e.getMessage());
if (e != null) {
e.printStackTrace();
}
}
public static void error(String message) {
log("ERROR", message);
}
private static void log(String level, String message) {
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
String logEntry = String.format("[%s] %s: %s%n", level, timestamp, message);
System.out.print(logEntry); // 同时输出到控制台
// 写入日志文件
try (FileWriter writer = new FileWriter(LOG_FILE_PATH, true)) {
writer.write(logEntry);
} catch (IOException e) {
System.err.println("Failed to write to log file: " + e.getMessage());
}
}
}
改进网络管理器的错误处理
修改 client/network/NetworkManager.java 以增强错误处理:
package client.network;
import client.utils.LogUtil;
import common.Message;
import javax.swing.*;
import java.io.*;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketTimeoutException;
/**
* 网络管理类 - 增强版
* 负责与服务器的网络通信,包含完善的错误处理
*/
public class NetworkManager {
private String serverIp;
private static final int SERVER_PORT = 8888;
private static final int CONNECTION_TIMEOUT = 10000; // 10秒连接超时
private static final int SOCKET_TIMEOUT = 30000; // 30秒读取超时
private Socket socket;
private ObjectOutputStream oos;
private ObjectInputStream ois;
public NetworkManager(String serverIp) {
this.serverIp = serverIp;
}
/**
* 连接到服务器
*/
public boolean connectToServer() {
try {
LogUtil.info("正在尝试连接到服务器: " + serverIp + ":" + SERVER_PORT);
socket = new Socket();
socket.connect(new java.net.InetSocketAddress(serverIp, SERVER_PORT), CONNECTION_TIMEOUT);
socket.setSoTimeout(SOCKET_TIMEOUT); // 设置读取超时
oos = new ObjectOutputStream(socket.getOutputStream());
ois = new ObjectInputStream(socket.getInputStream());
LogUtil.info("成功连接到服务器");
return true;
} catch (ConnectException e) {
LogUtil.error("连接被拒绝,请检查服务器是否运行", e);
JOptionPane.showMessageDialog(null,
"连接服务器失败:连接被拒绝,请检查服务器是否运行\n" + e.getMessage());
return false;
} catch (SocketTimeoutException e) {
LogUtil.error("连接超时", e);
JOptionPane.showMessageDialog(null,
"连接服务器失败:连接超时\n" + e.getMessage());
return false;
} catch (IOException e) {
LogUtil.error("网络连接异常", e);
JOptionPane.showMessageDialog(null,
"连接服务器失败:" + e.getMessage());
return false;
}
}
/**
* 发送消息
*/
public synchronized void sendMessage(Message message) {
try {
if (oos != null && !socket.isClosed() && !socket.isOutputShutdown()) {
oos.writeObject(message);
oos.flush();
LogUtil.info("消息发送成功: " + message.getType());
} else {
LogUtil.warning("无法发送消息,连接可能已断开");
throw new IOException("连接已断开");
}
} catch (IOException e) {
LogUtil.error("发送消息失败", e);
handleConnectionError("发送消息失败:" + e.getMessage());
}
}
/**
* 接收消息
*/
public Message receiveMessage() {
try {
if (ois != null && !socket.isClosed() && !socket.isInputShutdown()) {
Message message = (Message) ois.readObject();
if (message != null) {
LogUtil.info("接收到消息: " + message.getType());
}
return message;
} else {
LogUtil.warning("无法接收消息,连接可能已断开");
return null;
}
} catch (ClassNotFoundException e) {
LogUtil.error("接收消息时发生类找不到异常", e);
return null;
} catch (IOException e) {
LogUtil.error("接收消息时发生IO异常", e);
handleConnectionError("接收消息失败:" + e.getMessage());
return null;
}
}
/**
* 处理连接错误
*/
private void handleConnectionError(String errorMessage) {
JOptionPane.showMessageDialog(null, errorMessage + "\n请检查网络连接或重新登录。");
resetSocket(); // 清理连接资源
}
/**
* 重置Socket连接
*/
public synchronized void resetSocket() {
try {
LogUtil.info("正在清理网络连接资源");
if (ois != null) {
ois.close();
ois = null;
}
if (oos != null) {
oos.close();
oos = null;
}
if (socket != null) {
socket.close();
socket = null;
}
LogUtil.info("网络连接资源清理完成");
} catch (IOException e) {
LogUtil.error("清理网络连接资源时发生异常", e);
}
}
/**
* 检查连接状态
*/
public boolean isConnected() {
return socket != null && !socket.isClosed() && socket.isConnected() &&
!socket.isInputShutdown() && !socket.isOutputShutdown();
}
}
6.2 安全性改进
为了提高聊天室的安全性,我们可以对用户认证和消息传输进行改进:
改进认证管理器
修改 client/managers/AuthenticationManager.java 添加安全性:
package client.managers;
import client.ChatClient;
import client.utils.LogUtil;
import common.Message;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.swing.*;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
* 认证管理类 - 增强版
* 负责用户注册、登录、密码找回等功能,包含安全加密
*/
public class AuthenticationManager {
private final ChatClient chatClient;
public AuthenticationManager(ChatClient chatClient) {
this.chatClient = chatClient;
}
/**
* 对密码进行哈希处理
*/
private String hashPassword(String password) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hashedBytes = md.digest(password.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : hashedBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
LogUtil.error("密码哈希处理失败", e);
return password; // 回退到原始密码(仅用于演示)
}
}
/**
* 用户注册
*/
public void register(String account, String password) {
if (!validateCredentials(account, password)) {
return;
}
try {
String hashedPassword = hashPassword(password);
Message registerMsg = new Message(Message.Type.REGISTER, account, "");
registerMsg.setPassword(hashedPassword);
chatClient.getNetworkManager().sendMessage(registerMsg);
Message response = chatClient.getNetworkManager().receiveMessage();
if (response != null) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), response.getContent());
if (response.getContent().contains("成功")) {
chatClient.getLoginRegisterFrame().dispose();
}
} else {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "服务器无响应");
}
} catch (Exception ex) {
LogUtil.error("注册过程发生异常", ex);
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "注册失败:" + ex.getMessage());
}
}
/**
* 用户登录
*/
public void login(String account, String password, String serverIp) {
if (!validateCredentials(account, password)) {
return;
}
try {
chatClient.setServerIp(serverIp);
if (!chatClient.getNetworkManager().connectToServer()) {
return;
}
String hashedPassword = hashPassword(password);
Message loginMsg = new Message(Message.Type.LOGIN, account, "");
loginMsg.setPassword(hashedPassword);
chatClient.getNetworkManager().sendMessage(loginMsg);
Message response = chatClient.getNetworkManager().receiveMessage();
if (response != null) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), response.getContent());
if (response.getContent().contains("成功")) {
chatClient.setUsername(account);
chatClient.getLoginRegisterFrame().dispose();
chatClient.initChatUI();
chatClient.startMessageListener();
// 请求获取在线用户列表
Message getUsersMsg = new Message(Message.Type.GET_ONLINE_USERS, account, "", "");
chatClient.getNetworkManager().sendMessage(getUsersMsg);
} else {
chatClient.getLoginPwdField().setText("");
}
} else {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "服务器无响应");
chatClient.getLoginPwdField().setText("");
}
} catch (Exception ex) {
LogUtil.error("登录过程发生异常", ex);
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "登录失败:" + ex.getMessage());
chatClient.resetSocket();
}
}
/**
* 验证凭据的有效性
*/
private boolean validateCredentials(String account, String password) {
if (account == null || account.trim().isEmpty()) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "账号不能为空!");
return false;
}
if (password == null || password.trim().isEmpty()) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "密码不能为空!");
return false;
}
if (password.length() < 6) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "密码长度不能少于6位!");
return false;
}
if (account.length() > 20) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "账号长度不能超过20位!");
return false;
}
return true;
}
/**
* 找回密码
*/
public void findPassword(String account) {
if (account == null || account.trim().isEmpty()) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "账号不能为空!");
return;
}
try {
Message findMsg = new Message(Message.Type.FIND_PASSWORD, account, "");
chatClient.getNetworkManager().sendMessage(findMsg);
Message response = chatClient.getNetworkManager().receiveMessage();
if (response != null) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), response.getContent());
if (response.getContent().contains("验证通过")) {
resetPassword(account);
}
} else {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "服务器无响应");
}
} catch (Exception ex) {
LogUtil.error("找回密码过程发生异常", ex);
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "验证账号失败:" + ex.getMessage());
chatClient.resetSocket();
}
}
/**
* 重置密码
*/
public void resetPassword(String account) {
JPasswordField newPwdField = new JPasswordField();
int result = JOptionPane.showConfirmDialog(
chatClient.getLoginRegisterFrame(),
new Object[]{"请输入新密码(至少6位):", newPwdField},
"重置密码",
JOptionPane.OK_CANCEL_OPTION
);
if (result == JOptionPane.OK_OPTION) {
String newPassword = new String(newPwdField.getPassword()).trim();
if (newPassword.isEmpty()) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "新密码不能为空!");
return;
}
if (newPassword.length() < 6) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "新密码长度不能少于6位!");
return;
}
try {
String hashedPassword = hashPassword(newPassword);
Message resetMsg = new Message(Message.Type.RESET_PASSWORD, account, "");
resetMsg.setPassword(hashedPassword);
chatClient.getNetworkManager().sendMessage(resetMsg);
Message response = chatClient.getNetworkManager().receiveMessage();
if (response != null) {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), response.getContent());
if (response.getContent().contains("成功")) {
chatClient.getLoginPwdField().setText("");
}
} else {
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "服务器无响应");
}
} catch (Exception ex) {
LogUtil.error("重置密码过程发生异常", ex);
JOptionPane.showMessageDialog(chatClient.getLoginRegisterFrame(), "重置密码失败:" + ex.getMessage());
}
}
}
}
6.3 性能优化和内存管理
为了确保应用的长期稳定运行,需要考虑性能优化和内存管理:
改进消息处理器
修改 client/handler/MessageHandler.java 添加性能优化:
package client.handler;
import client.ChatClient;
import client.ui.ChatMainUI;
import client.utils.LogUtil;
import common.Message;
import javax.swing.*;
import java.awt.Color;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 消息处理器类 - 优化版
* 负责处理从服务器接收到的各种消息类型,包含性能优化
*/
public class MessageHandler {
private final ChatClient chatClient;
private final ChatMainUI chatMainUI;
private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
public MessageHandler(ChatClient chatClient, ChatMainUI chatMainUI) {
this.chatClient = chatClient;
this.chatMainUI = chatMainUI;
}
/**
* 处理消息
*/
public void handleMessage(Message message) {
SwingUtilities.invokeLater(() -> {
try {
switch (message.getType()) {
case PRIVATE_CHAT:
handlePrivateChat(message);
break;
case GROUP_CHAT:
handleGroupChat(message);
break;
case FILE_PRIVATE:
case FILE_GROUP:
handleFile(message);
break;
case SHAKE:
handleShake(message);
break;
case ONLINE_USERS:
handleOnlineUsers(message);
break;
case GROUP_LIST:
handleGroupList(message);
break;
case ONLINE_NOTIFY:
handleOnlineNotify(message);
break;
case OFFLINE_NOTIFY:
handleOfflineNotify(message);
break;
case CREATE_GROUP:
handleCreateGroup(message);
break;
case JOIN_GROUP:
handleJoinGroup(message);
break;
case SEARCH_GROUP:
handleSearchGroup(message);
break;
case REGISTER_RESPONSE:
handleRegisterResponse(message);
break;
case FIND_PASSWORD_RESPONSE:
handleFindPasswordResponse(message);
break;
case RESET_PASSWORD_RESPONSE:
handleResetPasswordResponse(message);
break;
default:
LogUtil.warning("未知的消息类型: " + message.getType());
break;
}
} catch (Exception e) {
LogUtil.error("处理消息时发生异常", e);
}
});
}
private void handlePrivateChat(Message message) {
String time = LocalDateTime.now().format(timeFormatter);
String formattedMessage = String.format(
"[%s] 【私聊】%s:%s%n",
time, message.getSender(), message.getContent()
);
appendToChatArea(formattedMessage, Color.BLUE);
}
private void handleGroupChat(Message message) {
String time = LocalDateTime.now().format(timeFormatter);
String formattedMessage = String.format(
"[%s] 【群聊-%s】%s:%s%n",
time, message.getGroupName() != null ? message.getGroupName() : message.getGroupId(),
message.getSender(), message.getContent()
);
appendToChatArea(formattedMessage, Color.GREEN);
}
private void handleFile(Message message) {
String time = LocalDateTime.now().format(timeFormatter);
String formattedMessage = String.format(
"[%s] 【文件传输】来自%s的文件:%s(%s)%n",
time, message.getSender(), message.getFileName(), formatFileSize(message.getFileSize())
);
appendToChatArea(formattedMessage, Color.ORANGE);
int confirm = JOptionPane.showConfirmDialog(
chatMainUI,
String.format("收到来自%s的文件:%s\n大小:%s\n是否保存?",
message.getSender(), message.getFileName(), formatFileSize(message.getFileSize())),
"文件接收",
JOptionPane.YES_NO_OPTION
);
if (confirm == JOptionPane.YES_OPTION) {
chatClient.saveBytesToFile(message.getFileData(), message.getFileName());
}
}
private void handleShake(Message message) {
String time = LocalDateTime.now().format(timeFormatter);
String formattedMessage = String.format(
"[%s] 【窗口抖动】%s向您发送了窗口抖动!%n",
time, message.getSender()
);
appendToChatArea(formattedMessage, Color.RED);
chatClient.shakeWindow();
}
private void handleOnlineUsers(Message message) {
if (message.getOnlineUsers() != null) {
chatClient.updateUserList(message.getOnlineUsers());
}
}
private void handleGroupList(Message message) {
if (message.getGroupList() != null) {
chatClient.updateGroupList(message.getGroupList());
}
}
private void handleOnlineNotify(Message message) {
String time = LocalDateTime.now().format(timeFormatter);
String formattedMessage = String.format(
"[%s] 【系统通知】%s 上线了%n",
time, message.getSender()
);
appendToChatArea(formattedMessage, Color.GRAY);
}
private void handleOfflineNotify(Message message) {
String time = LocalDateTime.now().format(timeFormatter);
String formattedMessage = String.format(
"[%s] 【系统通知】%s 下线了%n",
time, message.getSender()
);
appendToChatArea(formattedMessage, Color.GRAY);
}
// 新增处理方法
private void handleCreateGroup(Message message) {
String time = LocalDateTime.now().format(timeFormatter);
String formattedMessage = String.format(
"[%s] 【群组通知】%s%n",
time, message.getContent()
);
appendToChatArea(formattedMessage, Color.MAGENTA);
}
private void handleJoinGroup(Message message) {
String time = LocalDateTime.now().format(timeFormatter);
String formattedMessage = String.format(
"[%s] 【群组通知】%s%n",
time, message.getContent()
);
appendToChatArea(formattedMessage, Color.MAGENTA);
}
private void handleSearchGroup(Message message) {
// 搜索结果会在GROUP_LIST消息中处理
}
private void handleRegisterResponse(Message message) {
// 注册响应在认证管理器中处理
}
private void handleFindPasswordResponse(Message message) {
// 找回密码响应在认证管理器中处理
}
private void handleResetPasswordResponse(Message message) {
// 重置密码响应在认证管理器中处理
}
/**
* 将消息追加到聊天区域
*/
private void appendToChatArea(String message, Color color) {
JTextArea chatArea = chatMainUI.getChatArea();
if (chatArea != null) {
// 限制聊天区域的最大行数,避免内存溢出
int maxLines = 1000;
String currentText = chatArea.getText();
String[] lines = currentText.split("\n");
if (lines.length > maxLines) {
// 保留最新的900行,丢弃前面的
StringBuilder newText = new StringBuilder();
for (int i = lines.length - 900; i < lines.length; i++) {
if (i >= 0) {
newText.append(lines[i]).append("\n");
}
}
chatArea.setText(newText.toString());
}
// 设置文本颜色并追加消息
chatArea.append(message);
chatArea.setCaretPosition(chatArea.getDocument().getLength()); // 滚动到底部
}
}
/**
* 格式化文件大小
*/
private String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.2f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.2f MB", size / (1024.0 * 1024));
} else {
return String.format("%.2f GB", size / (1024.0 * 1024 * 1024));
}
}
}
6.4 配置文件管理
为了使应用更具可配置性,我们可以添加配置文件管理:
创建 client/config/AppConfig.java:
package client.config;
import java.io.*;
import java.util.Properties;
/**
* 应用配置类
* 管理应用程序的各种配置参数
*/
public class AppConfig {
private static final String CONFIG_FILE = "chatroom.properties";
private Properties properties;
public static final String DEFAULT_SERVER_IP = "127.0.0.1";
public static final int DEFAULT_SERVER_PORT = 8888;
public static final int DEFAULT_CONNECTION_TIMEOUT = 10000;
public static final int DEFAULT_SOCKET_TIMEOUT = 30000;
public static final int MAX_CHAT_HISTORY_LINES = 1000;
private static AppConfig instance;
private AppConfig() {
properties = new Properties();
loadConfig();
}
public static AppConfig getInstance() {
if (instance == null) {
instance = new AppConfig();
}
return instance;
}
/**
* 加载配置文件
*/
private void loadConfig() {
try (FileInputStream fis = new FileInputStream(CONFIG_FILE)) {
properties.load(fis);
} catch (IOException e) {
// 如果配置文件不存在,使用默认值
System.out.println("配置文件不存在,使用默认配置");
setDefaults();
}
}
/**
* 设置默认配置
*/
private void setDefaults() {
properties.setProperty("server.ip", DEFAULT_SERVER_IP);
properties.setProperty("server.port", String.valueOf(DEFAULT_SERVER_PORT));
properties.setProperty("connection.timeout", String.valueOf(DEFAULT_CONNECTION_TIMEOUT));
properties.setProperty("socket.timeout", String.valueOf(DEFAULT_SOCKET_TIMEOUT));
properties.setProperty("max.chat.history.lines", String.valueOf(MAX_CHAT_HISTORY_LINES));
}
/**
* 保存配置到文件
*/
public void saveConfig() {
try (FileOutputStream fos = new FileOutputStream(CONFIG_FILE)) {
properties.store(fos, "ChatRoom Application Configuration");
} catch (IOException e) {
System.err.println("保存配置文件失败: " + e.getMessage());
}
}
/**
* 获取字符串属性
*/
public String getStringProperty(String key, String defaultValue) {
return properties.getProperty(key, defaultValue);
}
/**
* 获取整数属性
*/
public int getIntProperty(String key, int defaultValue) {
String value = properties.getProperty(key);
try {
return value != null ? Integer.parseInt(value) : defaultValue;
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* 获取布尔属性
*/
public boolean getBooleanProperty(String key, boolean defaultValue) {
String value = properties.getProperty(key);
return value != null ? Boolean.parseBoolean(value) : defaultValue;
}
/**
* 设置属性
*/
public void setProperty(String key, String value) {
properties.setProperty(key, value);
}
// 便捷的获取方法
public String getServerIp() {
return getStringProperty("server.ip", DEFAULT_SERVER_IP);
}
public int getServerPort() {
return getIntProperty("server.port", DEFAULT_SERVER_PORT);
}
public int getConnectionTimeout() {
return getIntProperty("connection.timeout", DEFAULT_CONNECTION_TIMEOUT);
}
public int getSocketTimeout() {
return getIntProperty("socket.timeout", DEFAULT_SOCKET_TIMEOUT);
}
public int getMaxChatHistoryLines() {
return getIntProperty("max.chat.history.lines", MAX_CHAT_HISTORY_LINES);
}
}
6.5 单元测试
为确保代码质量,我们还需要为关键组件编写单元测试。以下是针对一些核心功能的测试示例:
创建 test/MessageTest.java:
package test;
import common.Message;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Message类的单元测试
*/
public class MessageTest {
@Test
public void testMessageCreation() {
Message msg = new Message(Message.Type.LOGIN, "user1", "user2", "hello");
assertEquals(Message.Type.LOGIN, msg.getType());
assertEquals("user1", msg.getSender());
assertEquals("user2", msg.getReceiver());
assertEquals("hello", msg.getContent());
}
@Test
public void testMessageWithConstructor() {
Message msg = new Message(Message.Type.LOGIN, "user1");
assertEquals(Message.Type.LOGIN, msg.getType());
assertEquals("user1", msg.getSender());
assertEquals("", msg.getContent());
}
@Test
public void testMessageSetterGetter() {
Message msg = new Message(Message.Type.PRIVATE_CHAT, "sender", "receiver", "content");
// 测试各种setter和getter
msg.setFileName("test.txt");
assertEquals("test.txt", msg.getFileName());
msg.setFileSize(1024L);
assertEquals(1024L, msg.getFileSize());
msg.setGroupId("group123");
assertEquals("group123", msg.getGroupId());
msg.setGroupName("Test Group");
assertEquals("Test Group", msg.getGroupName());
}
}
第七阶段:部署和维护
7.1 打包和发布
为了让应用更容易部署,我们可以创建一个构建脚本来打包应用:
创建 build.gradle:
plugins { id 'java' id 'application' } group = 'com.chatroom' version = '1.0.0' repositories { mavenCentral() } dependencies { implementation 'junit:junit:4.13.2' // 其他依赖项 } application { mainClass = 'client.ChatClient' } jar { manifest { attributes( 'Main-Class': 'client.ChatClient', 'Implementation-Version': version ) } from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } archiveFileName = "ChatRoom-${version}.jar" } // 创建发布目录 task createReleaseDir { doLast { mkdir 'release' } } // 复制JAR文件到发布目录 task release(type: Copy, dependsOn: ['jar', 'createReleaseDir']) { from 'build/libs/' into 'release/' include "*.jar" } // 创建运行脚本 task createRunScripts(type: Exec) { workingDir '.' commandLine 'bash', '-c', ''' echo "#!/bin/bash" > run_client.sh echo "java -jar ChatRoom-1.0.0.jar" >> run_client.sh chmod +x run_client.sh echo "@echo off" > run_client.bat echo "java -jar ChatRoom-1.0.0.jar" >> run_client.bat ''' }
7.2 文档完善
最后,让我们完善文档,包括常见问题解答和故障排除指南:
常见问题解答 (FAQ)
Q: 连接服务器失败怎么办?
A: 请检查以下几点:
- 确保服务端程序正在运行
- 检查防火墙设置,确保端口8888未被阻止
- 确认服务器IP地址正确
- 检查网络连接是否正常
Q: 如何修改默认端口号?
A: 修改 server/ChatServer.java 中的 PORT 常量,并相应地修改客户端连接代码。
Q: 如何添加新的消息类型?
A: 1. 在 common/Message.java 的 Type 枚举中添加新类型
2. 在服务端 MessageHandler.java 中添加相应的处理方法
3. 在客户端 MessageHandler.java 中添加相应的处理方法
Q: 如何添加用户权限管理?
A: 可以在 UserManager.java 中扩展用户角色和权限功能:
public enum UserRole {
ADMIN, MODERATOR, USER
}
public class User {
private String username;
private String passwordHash;
private UserRole role;
// ... 其他字段
}
故障排除
问题:客户端启动后立即退出
解决方案:
- 检查控制台是否有错误信息
- 确认网络连接正常
- 检查服务器是否运行在正确的端口上
问题:消息发送后对方收不到
解决方案:
- 确认接收方在线
- 检查消息格式是否正确
- 查看服务器日志确认消息是否正确转发
问题:界面显示异常
解决方案:
- 尝试调整JVM的GUI相关参数
- 更新显卡驱动程序
- 检查系统缩放设置
性能调优建议
内存优化
- 限制聊天历史记录的最大行数
- 及时释放不需要的对象引用
- 使用对象池技术减少GC压力
网络优化
- 实现心跳机制检测连接状态
- 添加消息压缩功能
- 使用异步I/O提高并发性能
UI响应优化
- 将耗时操作放在后台线程执行
- 使用适当的布局管理器
- 避免在EDT中执行阻塞操作
第八阶段:系统集成与测试
8.1 单元测试
为了确保系统的可靠性,我们需要为关键组件编写单元测试。以下是几个重要组件的测试用例:
package test;
import common.Message;
import common.Group;
import org.junit.Test;
import static org.junit.Assert.*;
public class MessageTest {
@Test
public void testMessageCreation() {
Message msg = new Message(Message.Type.LOGIN, "user1", "user2", "hello");
assertEquals(Message.Type.LOGIN, msg.getType());
assertEquals("user1", msg.getSender());
assertEquals("user2", msg.getReceiver());
assertEquals("hello", msg.getContent());
}
@Test
public void testGroupCreation() {
Group group = new Group("group123", "Test Group");
assertEquals("group123", group.getGroupId());
assertEquals("Test Group", group.getGroupName());
group.addMember("user1");
assertTrue(group.getMembers().contains("user1"));
}
}
8.2 集成测试
系统集成测试流程图:
graph TD A[启动服务端] --> B[客户端连接] B --> C[用户注册测试] C --> D[用户登录测试] D --> E[消息发送接收测试] E --> F[群组功能测试] F --> G[文件传输测试] G --> H[窗口抖动测试] H --> I[登出断开连接] I --> J[关闭服务端] C --> C1{注册是否成功?} D --> D1{登录是否成功?} E --> E1{消息是否正确传递?} F --> F1{群组操作是否正常?} G --> G1{文件是否正确传输?} H --> H1{抖动效果是否正常?} C1 -->|否| K[测试失败] D1 -->|否| K E1 -->|否| K F1 -->|否| K G1 -->|否| K H1 -->|否| K C1 -->|是| D D1 -->|是| E E1 -->|是| F F1 -->|是| G G1 -->|是| H H1 -->|是| I style A fill:#e3f2fd style K fill:#ffcdd2 style C1 fill:#ffccbc style D1 fill:#ffccbc
8.3 性能测试
系统性能测试指标:
- 连接建立时间:客户端连接到服务端的时间应小于1秒
- 消息响应时间:消息从发送到接收的平均延迟应小于100毫秒
- 并发处理能力:服务端应能同时处理至少100个客户端连接
- 内存使用:单个客户端运行时内存占用应小于100MB
- CPU使用率:空闲状态下CPU使用率应低于5%
项目特点
代码结构清晰
- 采用分层架构,职责分离
- Manager类负责业务逻辑
- Handler类负责消息处理
- UI类负责界面展示
graph TB subgraph "表现层" A1[UI组件] A2[UI工厂] end subgraph "控制层" B1[主控制器] B2[消息监听器] end subgraph "业务层" C1[ChatManager] C2[FileManager] C3[DataManager] C4[WindowManager] C5[AuthenticationManager] end subgraph "网络层" D1[NetworkManager] D2[MessageHandler] end A1 --> B1 A2 --> B1 B1 --> C1 B1 --> C2 B1 --> C3 B1 --> C4 B1 --> C5 B2 --> D2 C1 --> D1 C2 --> D1 C5 --> D1 style A1 fill:#e3f2fd style B1 fill:#f3e5f5 style C1 fill:#e8f5e8 style D1 fill:#fff3e0
可扩展性强
- 消息类型使用枚举,易于扩展
- Manager类独立,便于添加新功能
- 网络通信封装,便于更换协议
线程安全
- 使用ConcurrentHashMap保证并发安全
- Swing组件更新在事件调度线程中执行
- 消息监听独立线程运行
用户体验
- 美化的UI界面
- 圆角按钮和面板
- 实时消息更新
- 文件传输进度提示
系统架构图总结
整个系统的完整架构图如下:
graph TB subgraph "客户端" A1[ChatClient] A2[MessageListener] A3[UI Components] end subgraph "客户端网络" B1[NetworkManager] end subgraph "客户端业务" C1[ChatManager] C2[FileManager] C3[DataManager] C4[WindowManager] C5[AuthenticationManager] end subgraph "客户端处理" D1[MessageHandler] end subgraph "通信层" E1[Message Objects] end subgraph "服务端" F1[ChatServer] F2[ClientHandler] F3[ServerMessageHandler] end subgraph "服务端管理" G1[UserManager] G2[OnlineUserManager] G3[GroupManager] end subgraph "服务端服务" H1[BroadcastService] end A1 --> B1 A1 --> C1 A1 --> C2 A1 --> C3 A1 --> C4 A1 --> C5 A2 --> D1 B1 --> E1 C1 --> E1 C2 --> E1 C5 --> E1 E1 --> F2 F1 --> F2 F2 --> F3 F3 --> G1 F3 --> G2 F3 --> G3 F3 --> H1 G1 --> H1 G2 --> H1 G3 --> H1 style A1 fill:#e3f2fd style F1 fill:#ffebee style E1 fill:#f1f8e9
最佳实践和设计模式
使用的设计模式
- 工厂模式:UIComponentFactory用于创建统一风格的UI组件
- 观察者模式:MessageListener监听服务端消息
- 策略模式:Message.Type枚举定义不同的消息处理策略
- 单例模式:可通过扩展实现配置管理单例
- 命令模式:各种Manager类封装不同的业务操作
代码质量保证
- 命名规范:遵循Java命名约定,类名使用大驼峰,方法名使用小驼峰
- 注释规范:重要类和方法都有详细的JavaDoc注释
- 异常处理:在网络通信和文件操作中有完善的异常处理
- 资源管理:及时关闭IO流和网络连接,防止资源泄漏
扩展功能建议
高级功能
- 消息加密:实现端到端加密
- 离线消息:存储离线期间的消息
- 表情包支持:添加富文本消息功能
- 语音消息:集成语音录制和播放功能
- 视频通话:添加简单的视频聊天功能
架构改进
- 微服务化:将不同功能模块拆分为独立服务
- 数据库集成:使用MySQL或PostgreSQL存储数据
- 消息队列:使用Redis或RabbitMQ处理消息
- 负载均衡:支持多个服务器实例
- 监控系统:集成应用性能监控
总结
本教程详细介绍了如何从零开始开发一个功能完整的Java Socket聊天室应用。通过本教程,你将学到:
- Java Socket网络编程基础 - 包括TCP/IP连接、数据传输、异常处理等
- 多线程编程处理并发 - 客户端消息监听、服务端多客户端处理
- Swing GUI开发技巧 - 界面设计、事件处理、用户体验优化
- 面向对象设计原则 - 分层架构、单一职责、开闭原则
- 项目架构设计方法 - MVC模式、组件化设计、接口抽象
- 错误处理和日志记录 - 完善的异常处理机制
- 安全性考虑 - 密码加密、输入验证
- 性能优化 - 内存管理、网络优化、UI响应优化
- 部署和维护 - 构建脚本、配置管理、故障排除
- 测试策略 - 单元测试、集成测试、性能测试
这个聊天室应用涵盖了网络编程、GUI开发、多线程处理等多个重要领域,是学习Java综合应用开发的良好实践项目。通过扩展和完善,你可以将其发展为功能更丰富的即时通讯应用。
希望这个详细的教程能帮助你理解并开发出自己的Java聊天室应用!
知识点测试
读完文章了?来测试一下你对知识点的掌握程度吧!
评论区
使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。
如果评论系统无法加载,请确保:
- 您的网络可以访问 GitHub
- giscus GitHub App 已安装到仓库
- 仓库已启用 Discussions 功能